@pixerate/editor 0.23.0 → 0.24.0
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.
- package/README.md +96 -1
- package/THIRD_PARTY_NOTICES.md +51 -0
- package/dist/agent/index.cjs +9 -8
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +4 -3
- package/dist/agent/index.d.ts +4 -3
- package/dist/agent/index.js +3 -2
- package/dist/agent/index.js.map +1 -1
- package/dist/canvas/index.cjs +1210 -17
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +318 -1
- package/dist/canvas/index.d.ts +318 -1
- package/dist/canvas/index.js +1199 -6
- package/dist/canvas/index.js.map +1 -1
- package/dist/{chunk-QBJ22YZR.js → chunk-3CN2WD3X.js} +1354 -206
- package/dist/chunk-3CN2WD3X.js.map +1 -0
- package/dist/{chunk-JJDLPEWL.js → chunk-3LV6RRPH.js} +2 -2
- package/dist/{chunk-MJZBNWG7.js → chunk-5S3Z4PPF.js} +78 -557
- package/dist/chunk-5S3Z4PPF.js.map +1 -0
- package/dist/chunk-664LQYFJ.js +115 -0
- package/dist/chunk-664LQYFJ.js.map +1 -0
- package/dist/{chunk-AA4ZEVCC.cjs → chunk-D27YFC46.cjs} +101 -580
- package/dist/chunk-D27YFC46.cjs.map +1 -0
- package/dist/chunk-D6FH2YAX.cjs +131 -0
- package/dist/chunk-D6FH2YAX.cjs.map +1 -0
- package/dist/{chunk-4SUNJZWB.cjs → chunk-FD4LPFAE.cjs} +48 -200
- package/dist/chunk-FD4LPFAE.cjs.map +1 -0
- package/dist/chunk-FODNLX5N.js +749 -0
- package/dist/chunk-FODNLX5N.js.map +1 -0
- package/dist/{chunk-OYXSKQFR.js → chunk-FPENXAEJ.js} +35 -187
- package/dist/chunk-FPENXAEJ.js.map +1 -0
- package/dist/chunk-HLIMTC6N.js +1 -0
- package/dist/chunk-HLIMTC6N.js.map +1 -0
- package/dist/{chunk-QGHGJ56O.js → chunk-ITBSKI26.js} +1483 -19
- package/dist/chunk-ITBSKI26.js.map +1 -0
- package/dist/{chunk-XDMXXC4Z.cjs → chunk-MA2F5KHF.cjs} +350 -227
- package/dist/chunk-MA2F5KHF.cjs.map +1 -0
- package/dist/{chunk-RIKBJLS2.cjs → chunk-MCSJFWAO.cjs} +1356 -208
- package/dist/chunk-MCSJFWAO.cjs.map +1 -0
- package/dist/chunk-MKA3AHLV.cjs +256 -0
- package/dist/chunk-MKA3AHLV.cjs.map +1 -0
- package/dist/chunk-OLZJXL4R.cjs +749 -0
- package/dist/chunk-OLZJXL4R.cjs.map +1 -0
- package/dist/chunk-OXSV64JX.js +689 -0
- package/dist/chunk-OXSV64JX.js.map +1 -0
- package/dist/chunk-P3RQ5UJE.cjs +689 -0
- package/dist/chunk-P3RQ5UJE.cjs.map +1 -0
- package/dist/chunk-P6ODPZ4W.js +256 -0
- package/dist/chunk-P6ODPZ4W.js.map +1 -0
- package/dist/chunk-PGNZKFQS.js +685 -0
- package/dist/chunk-PGNZKFQS.js.map +1 -0
- package/dist/chunk-PL4JB3BF.cjs +1 -0
- package/dist/chunk-PL4JB3BF.cjs.map +1 -0
- package/dist/{chunk-GVSMRPHB.cjs → chunk-SBCZ6RN5.cjs} +1568 -104
- package/dist/chunk-SBCZ6RN5.cjs.map +1 -0
- package/dist/{chunk-CL37JGFW.cjs → chunk-SML64VJG.cjs} +4 -4
- package/dist/{chunk-CL37JGFW.cjs.map → chunk-SML64VJG.cjs.map} +1 -1
- package/dist/chunk-TNNJXF46.cjs +115 -0
- package/dist/chunk-TNNJXF46.cjs.map +1 -0
- package/dist/chunk-U5HZSFCS.js +131 -0
- package/dist/chunk-U5HZSFCS.js.map +1 -0
- package/dist/{chunk-K3R5NG2D.js → chunk-U5KWZU4X.js} +251 -128
- package/dist/chunk-U5KWZU4X.js.map +1 -0
- package/dist/chunk-XNORY7M6.cjs +685 -0
- package/dist/chunk-XNORY7M6.cjs.map +1 -0
- package/dist/commands/index.d.cts +5 -353
- package/dist/commands/index.d.ts +5 -353
- package/dist/comment-store-BEMgVlAA.d.cts +135 -0
- package/dist/comment-store-BEMgVlAA.d.ts +135 -0
- package/dist/compare/index.cjs +15 -0
- package/dist/compare/index.cjs.map +1 -0
- package/dist/compare/index.d.cts +58 -0
- package/dist/compare/index.d.ts +58 -0
- package/dist/compare/index.js +15 -0
- package/dist/compare/index.js.map +1 -0
- package/dist/{controller-pePCJLT1.d.cts → controller-BhLLAoi8.d.cts} +45 -4
- package/dist/{controller-pePCJLT1.d.ts → controller-BhLLAoi8.d.ts} +45 -4
- package/dist/documents-BQjzJNe2.d.ts +76 -0
- package/dist/documents-KRTPhvst.d.cts +76 -0
- package/dist/extensions/index.cjs +53 -5
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.d.cts +7 -3
- package/dist/extensions/index.d.ts +7 -3
- package/dist/extensions/index.js +52 -4
- package/dist/generation/index.cjs +1373 -0
- package/dist/generation/index.cjs.map +1 -0
- package/dist/generation/index.d.cts +554 -0
- package/dist/generation/index.d.ts +554 -0
- package/dist/generation/index.js +1373 -0
- package/dist/generation/index.js.map +1 -0
- package/dist/grammar/index.cjs +48 -3
- package/dist/grammar/index.cjs.map +1 -1
- package/dist/grammar/index.d.cts +322 -100
- package/dist/grammar/index.d.ts +322 -100
- package/dist/grammar/index.js +54 -9
- package/dist/image-editor/index.cjs +1831 -30
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +590 -3
- package/dist/image-editor/index.d.ts +590 -3
- package/dist/image-editor/index.js +1807 -6
- package/dist/image-editor/index.js.map +1 -1
- package/dist/{index-CCr_z9kF.d.cts → index-BhJBSm2d.d.cts} +370 -4
- package/dist/{index-Ck6ajwXh.d.ts → index-fJrlkJCd.d.ts} +370 -4
- package/dist/index.cjs +162 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -8
- package/dist/index.d.ts +14 -8
- package/dist/index.js +174 -37
- package/dist/index.js.map +1 -1
- package/dist/migrate-iTlm3a8h.d.cts +112 -0
- package/dist/migrate-iTlm3a8h.d.ts +112 -0
- package/dist/persistence/index.cjs +676 -0
- package/dist/persistence/index.cjs.map +1 -0
- package/dist/persistence/index.d.cts +347 -0
- package/dist/persistence/index.d.ts +347 -0
- package/dist/persistence/index.js +676 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/{registry-D5-R90p0.d.cts → registry-BDe-vlv3.d.ts} +2 -40
- package/dist/registry-D32LpIc4.d.cts +354 -0
- package/dist/{registry-D5-R90p0.d.ts → registry-DnzpgY27.d.cts} +2 -40
- package/dist/registry-evKTP4ls.d.ts +354 -0
- package/dist/schema-DYDGdYL9.d.cts +41 -0
- package/dist/schema-DYDGdYL9.d.ts +41 -0
- package/dist/serializers/index.cjs +4 -3
- package/dist/serializers/index.cjs.map +1 -1
- package/dist/serializers/index.d.cts +2 -1
- package/dist/serializers/index.d.ts +2 -1
- package/dist/serializers/index.js +3 -2
- package/dist/sourcemap-C8AH3MBQ.d.cts +91 -0
- package/dist/sourcemap-DA1Evhrv.d.ts +91 -0
- package/dist/spreadsheet/index.cjs +37 -3
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.d.cts +188 -17
- package/dist/spreadsheet/index.d.ts +188 -17
- package/dist/spreadsheet/index.js +43 -9
- package/dist/{tokens-CyLZmIei.d.cts → tokens-D7yXvUU6.d.cts} +15 -2
- package/dist/{tokens-CyLZmIei.d.ts → tokens-D7yXvUU6.d.ts} +15 -2
- package/dist/{track-changes-BSF58RK0.d.ts → track-changes-BK5sVf4_.d.cts} +18 -136
- package/dist/{track-changes-BSF58RK0.d.cts → track-changes-CDjCrwKe.d.ts} +18 -136
- package/package.json +16 -1
- package/dist/chunk-4SUNJZWB.cjs.map +0 -1
- package/dist/chunk-AA4ZEVCC.cjs.map +0 -1
- package/dist/chunk-AXV2ZW34.cjs +0 -118
- package/dist/chunk-AXV2ZW34.cjs.map +0 -1
- package/dist/chunk-B2ZRLN4S.js +0 -118
- package/dist/chunk-B2ZRLN4S.js.map +0 -1
- package/dist/chunk-GVSMRPHB.cjs.map +0 -1
- package/dist/chunk-K3R5NG2D.js.map +0 -1
- package/dist/chunk-MJZBNWG7.js.map +0 -1
- package/dist/chunk-OYXSKQFR.js.map +0 -1
- package/dist/chunk-QBJ22YZR.js.map +0 -1
- package/dist/chunk-QGHGJ56O.js.map +0 -1
- package/dist/chunk-RIKBJLS2.cjs.map +0 -1
- package/dist/chunk-XDMXXC4Z.cjs.map +0 -1
- /package/dist/{chunk-JJDLPEWL.js.map → chunk-3LV6RRPH.js.map} +0 -0
package/README.md
CHANGED
|
@@ -119,6 +119,26 @@ editor.commands.acceptChange(id); // rejectChange, acceptAllChanges, rejectAllCh
|
|
|
119
119
|
- **Serializers** render tracked content as if all changes were accepted: `getEditorText`, `htmlToPlainText` and `getEditorMarkdown` drop deleted text and use the new formatting and block types by default. Pass `{ trackedChanges: "original" }` (as if rejected) or `"all"` (both). `resolveTrackedChanges(doc, view)` returns a resolved document. TipTap's own `editor.getText()` still includes deleted text.
|
|
120
120
|
- Both extensions are also available as `createRichTextPreset({ comments: true, trackChanges: true })`. Mark styles use Tailwind classes; override them via `commentClass` / `activeClass` and `insertionHTMLAttributes` / `deletionHTMLAttributes` / `formatChangeHTMLAttributes` / `blockChangeClass`.
|
|
121
121
|
|
|
122
|
+
## Find and replace
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { createRichTextPreset, getFindReplaceState } from "@pixerate/editor";
|
|
126
|
+
|
|
127
|
+
const editor = new Editor({ extensions: [...createRichTextPreset({ findReplace: true })] }); // or FindReplace.configure({...})
|
|
128
|
+
|
|
129
|
+
editor.commands.setSearch({ query: "([A-Z][a-z]@), ([A-Z][a-z]@)", replacement: "\\2 \\1", options: { wildcards: true } });
|
|
130
|
+
editor.commands.findNext(); // findPrevious, replaceCurrent, replaceAll (one undo step), clearSearch
|
|
131
|
+
editor.commands.openFindReplace({ mode: "replace" }); // what Mod+F / Mod+H do; closeFindReplace hides it
|
|
132
|
+
getFindReplaceState(editor); // { open, mode, query, replacement, options, matches, currentIndex, error, truncated, lastReplaceCount }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- **Options** (`FindOptions`): `matchCase`, `wholeWord`, `matchPrefix`, `matchSuffix`, `ignorePunctuation`, `ignoreWhitespace`, `wildcards`, `regex`, `literal` (no `^` codes).
|
|
136
|
+
- **Special characters** (Word): `^p` paragraph mark (a pattern with `^p` reads sibling paragraphs together; `^p` in the replacement splits the paragraph), `^t` tab, `^l` line break, `^?` any character, `^#` digit, `^$` letter, `^w` white space, `^^` caret, `^+` / `^=` em / en dash, `^s` non-breaking space, `^13` / `^u8212` character codes. In Replace with: `^&` found text, `^c` clipboard (`getClipboardText` option).
|
|
137
|
+
- **Wildcards** (Word): `?`, `*` (shortest run within a paragraph), `[a-z]`, `[!x]`, `{n}`, `{n,}`, `{n,m}`, `@` (one or more), `<` / `>` word start / end, `( )` groups used as `\1`–`\9` in Replace with. Wildcard search always matches case.
|
|
138
|
+
- **Bounded**: special and wildcard patterns run on a linear-time matcher (no catastrophic backtracking); every search also has a time budget (`timeLimitMs`, 250 ms) and match cap (`maxMatches`, 10 000), reported as `truncated`. Raw `regex` mode uses JavaScript's `RegExp`, rejects nested repetition such as `(a+)+` and is only bounded between matches. Invalid patterns are reported as `error: { code, message }`, never thrown.
|
|
139
|
+
- **Track changes**: tracked-deleted text is skipped (a match never spans a deletion, but word boundaries see the live text), and replacements made while tracking become tracked deletions and insertions.
|
|
140
|
+
- Matches are decorated with `pixerate-find-match` and the current one also with `pixerate-find-match-current` (`matchClass` / `currentMatchClass`); style them in your CSS. `createFindReplaceCommands(editor)` returns command-registry specs (`findReplace.open`, `.next`, `.find`, `.replaceAll`, …).
|
|
141
|
+
|
|
122
142
|
## File import and rich clipboard
|
|
123
143
|
|
|
124
144
|
An import adapter registry maps file types (MIME types and extensions) to converters. Built-in `document` adapters cover markdown (`.md`, `.markdown`, `.mdown`, `.mkdn`, `.mkd`, `.Rmd`, `.md.txt`), HTML (sanitized with the same allowlist as markdown), plain text (`.txt`), RTF (best-effort text extraction) and DOCX (`.docx`, `.docm`, `.dotx`). The longest matching extension wins, then exact MIME types, then `priority`, then the most recent registration.
|
|
@@ -207,6 +227,46 @@ formatShortcut("Mod+Shift+Z", "mac"); // "⇧⌘Z" ("Ctrl+Shift+Z" on Windows /
|
|
|
207
227
|
- **Palette**: `rankCommands(query, entries, { recents })` does subsequence fuzzy ranking over label, group, keywords and id. Exact matches come first, and an empty query lists recent commands first. `createCommandPaletteController(registry)` holds the palette state that the React and Svelte `<CommandPalette>` components render.
|
|
208
228
|
- **Agents**: `createCommandToolProvider(registry)` adds `list_commands`, `run_command` and `run_commands` to a `ToolRegistry`, with complete MCP annotations. `run_commands` runs steps in order but is **not atomic**: steps that succeeded before a failure stay applied. To get all-or-nothing behaviour, register one `undoable` command that runs the steps.
|
|
209
229
|
|
|
230
|
+
## Generation models, planning and provenance
|
|
231
|
+
|
|
232
|
+
`@pixerate/editor/generation` describes generative models as data, checks a request against a model before it is sent, and records what produced each output. It reaches only the grammar and the agent tool-registry modules, so it has no third-party imports.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import {
|
|
236
|
+
planGenerationRequest,
|
|
237
|
+
capabilitiesToJsonSchema,
|
|
238
|
+
createGenerationToolProvider,
|
|
239
|
+
createProvenance,
|
|
240
|
+
recreateRequest,
|
|
241
|
+
estimateJobProgress,
|
|
242
|
+
expectedDurationFor,
|
|
243
|
+
type ModelCapabilities,
|
|
244
|
+
} from "@pixerate/editor/generation";
|
|
245
|
+
|
|
246
|
+
const model: ModelCapabilities = {
|
|
247
|
+
id: "img-xl",
|
|
248
|
+
media: "image",
|
|
249
|
+
prompt: { maxLength: 1000 }, // counted after templates/variables resolve (countResolved defaults to true)
|
|
250
|
+
negativePrompt: true,
|
|
251
|
+
aspectRatios: { options: ["9:16", "1:1", "16:9"], default: "1:1" },
|
|
252
|
+
quality: { options: ["draft", "standard", "high"] },
|
|
253
|
+
count: { max: 4, options: [1, 2, 4] },
|
|
254
|
+
references: { roles: { style_ref: { max: 2 }, character_ref: { max: 1 } }, maxTotal: 2 },
|
|
255
|
+
params: { guidance: { type: "number", min: 1, max: 10, step: 0.5 } },
|
|
256
|
+
timing: { baseMs: 12_000, perItemMs: 4_000 },
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const plan = planGenerationRequest({ aspectRatio: "21:9", count: 3 }, model, { strategy: "downgrade", resolvedPrompt });
|
|
260
|
+
// plan.request: { model: "img-xl", aspectRatio: "16:9", count: 2 }
|
|
261
|
+
// plan.adjustments: [{ field: "aspectRatio", from: "21:9", to: "16:9", kind: "snapped", reason }, { field: "count", … }]
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
- **Strategies**: `nearest` (default) snaps enum values to the closest option, clamps and rounds ranges, truncates text and reference lists, and drops unsupported fields and references, reporting each change. Ties go to the higher value, or to the lower one with `downgrade`. `upgrade` also breaks ties upwards but does not drop references the model cannot use: that is an error, as is a text-only model given image references. `error` changes nothing and reports every mismatch. Missing required references, unsupported modes (`edit` / `inpaint`) and a model mismatch are always errors. Planning is pure and deterministic.
|
|
265
|
+
- **Schema and agents**: `capabilitiesToJsonSchema(model)` builds a strict request schema (enums, `minimum` / `maximum`, `maxItems`, and descriptions for per-role limits) that `validateJsonSchema` checks. `createGenerationToolProvider({ models, generate, estimate? })` adds `list_models`, `plan_generation` and `estimate_generation` (read-only) and `generate` (destructive) to a `ToolRegistry`. `generate` requires an `idempotencyKey`: a retry with the same key and arguments returns the first result (or joins it while it is still running) instead of generating again. `mergeCapabilities(remote, overlay)` keeps server capability values and takes only presentation fields (`name`, `description`, `badges`, `icon`, `group`) and overlay-only models from the overlay.
|
|
266
|
+
- **Provenance**: `createProvenance({ rawPrompt, templates, bindings, values, model, plan })` pins the exact bodies and versions of every template the prompt reached, and records the variable values, the resolved prompt with its source map, the planned settings, adjustments and references. `serializeProvenance` / `parseProvenance` are versioned. `recreateRequest(provenance)` returns the request that was sent, and `replayProvenancePrompt` re-resolves the prompt from the pinned bodies.
|
|
267
|
+
- **Lookups and progress**: `createBatchResolver({ fetch, maxBatch, delayMs })` batches and dedupes lookups; a failed batch resolves its keys as empty instead of retrying. `estimateJobProgress({ startedAt, now, expectedMs })` returns a time-based percent that stays at or below 95 until `done`, and a label such as `~2m left` or `Almost done…`. `expectedDurationFor(model, request)` uses the model's `timing`.
|
|
268
|
+
- **Forms**: `getGenerationControls(model)` and `getReferenceSlots(model, request)` describe the controls that the React and Svelte `<GenerationSettings>` render.
|
|
269
|
+
|
|
210
270
|
### Document structure and autocorrect
|
|
211
271
|
|
|
212
272
|
Opt-in preset options add Word/Pages-style document structure:
|
|
@@ -239,8 +299,43 @@ Each module is also available as a subpath import:
|
|
|
239
299
|
| `@pixerate/editor/canvas` | Node-graph layout (needs the optional `@dagrejs/dagre` peer), placement, trajectories, displacement, clipboard and streaming layout helpers |
|
|
240
300
|
| `@pixerate/editor/agent` | MCP tool schemas, a planner for agent-driven canvas edits, and the dynamic `ToolRegistry` with editor/spreadsheet tool providers |
|
|
241
301
|
| `@pixerate/editor/commands` | `CommandRegistry`, `ShortcutMap`, palette ranking, `createCommandToolProvider` and built-in history / arrange / spreadsheet commands (no third-party imports) |
|
|
302
|
+
| `@pixerate/editor/persistence` | Autosave and crash recovery (`createAutosaveController`, `createRecoveryPolicy`, `createRevisionTracker`), storage adapters (memory, localStorage, IndexedDB, OPFS) and document migration (`migrateDocument`, `loadSections`, `LoadReport`) (no third-party imports) |
|
|
303
|
+
| `@pixerate/editor/generation` | Model capability descriptors, `planGenerationRequest`, `capabilitiesToJsonSchema`, `createGenerationToolProvider`, provenance, `createBatchResolver` and job-progress estimates (no third-party imports) |
|
|
304
|
+
|
|
305
|
+
The root entry (`@pixerate/editor`) re-exports everything except canvas, agent, commands, persistence, generation and image editor, including TipTap. Import from the narrowest subpath you need: `grammar`, `history`, `spreadsheet`, `commands`, `persistence` and `image-editor` have no third-party imports, so a spreadsheet-only app bundles no TipTap, markdown or dagre code. The package is marked `sideEffects: false`, so unused root exports are also tree-shaken.
|
|
306
|
+
|
|
307
|
+
## Autosave, crash recovery and document migration
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
import {
|
|
311
|
+
createAutosaveController,
|
|
312
|
+
createIndexedDbAdapter,
|
|
313
|
+
createRevisionTracker,
|
|
314
|
+
formatRecoveryMessage,
|
|
315
|
+
} from "@pixerate/editor/persistence";
|
|
316
|
+
|
|
317
|
+
const revisions = createRevisionTracker({ history, dirty: bindDirtyToHistory({ history, getValue }) });
|
|
318
|
+
const autosave = createAutosaveController({
|
|
319
|
+
storage: createIndexedDbAdapter("my-app"), // or createLocalStorageAdapter / createOpfsAdapter / createMemoryStorage
|
|
320
|
+
key: "doc-42",
|
|
321
|
+
getSnapshot: () => doc,
|
|
322
|
+
revision: revisions.revision,
|
|
323
|
+
savedRevision: revisions.savedRevision,
|
|
324
|
+
name: "Quarterly plan",
|
|
325
|
+
});
|
|
326
|
+
revisions.subscribe(() => autosave.notify());
|
|
327
|
+
|
|
328
|
+
const recovered = await autosave.checkRecovery(); // { snapshot, meta: { time, name, revision } } | null
|
|
329
|
+
if (recovered && confirm(formatRecoveryMessage(recovered.meta))) load(recovered.snapshot);
|
|
330
|
+
else if (recovered) await autosave.discardRecovery();
|
|
331
|
+
autosave.ready(); // hydration gate: nothing is written before this
|
|
332
|
+
addEventListener("pagehide", () => autosave.flushNow());
|
|
333
|
+
// after an explicit save: autosave.markClean() retires the snapshot, but only one this session wrote
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Snapshots are written `debounceMs` after the last edit and at least every `interval` while edits keep coming; failed writes retry with exponential backoff (`retryBaseMs` doubling to `retryMaxMs`). `createRecoveryPolicy` is the pure, clock-injected state machine behind it.
|
|
242
337
|
|
|
243
|
-
|
|
338
|
+
`migrateDocument(json, { currentVersion, steps: { 1: (doc) => … } })` upgrades stored JSON one version at a time and returns a `LoadReport`: `migratedFrom`, `readOnly` (a newer version, or a failed step: show it but never save over it) and `unknownKeys`. `loadSections(doc, { key: { parse, defaults } })` falls back to defaults one section at a time and keeps unknown keys; `loadDocument` does both. `loadImageEditorDocument(json)` (`@pixerate/editor/image-editor`) applies them to `SerializedImageEditorState`.
|
|
244
339
|
|
|
245
340
|
## History gestures, marks and dirty tracking
|
|
246
341
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -8,12 +8,24 @@ All sources below are from the ArtCraft "craft" apps, each licensed **MIT OR Apa
|
|
|
8
8
|
© 2026 ArtCraft Team and the respective app's contributors. They are used here under the MIT licence
|
|
9
9
|
reproduced at the end of this file. Paths are relative to each source repository.
|
|
10
10
|
|
|
11
|
+
## craft-libs
|
|
12
|
+
|
|
13
|
+
© 2026 ArtCraft Team and the craft-libs contributors.
|
|
14
|
+
|
|
15
|
+
- `crates/settings/src/migrate.rs` (`upgrade`: step-wise migration to a target version, missing version taken as current, newer version left alone) → `migrateDocument` in `packages/core/src/persistence/migrate.ts`
|
|
16
|
+
- `crates/settings/src/document.rs` (`deserialize_sections`: per-section fallback to defaults) → `loadSections` in `packages/core/src/persistence/migrate.ts`
|
|
17
|
+
- `crates/settings/src/error.rs` (`LoadReport`, `has_problems`, `SectionError`, read-only reasons) → `packages/core/src/persistence/migrate.ts`
|
|
18
|
+
- `crates/settings/src/store.rs` (debounced save, `retry_delay` exponential backoff with cap) → `packages/core/src/persistence/autosave.ts`, `exponentialBackoff` in `packages/core/src/persistence/recoveryPolicy.ts`
|
|
19
|
+
- Test cases from `crates/settings/tests/migrate.rs`, `tests/recovery.rs` and `tests/store.rs`, re-expressed → `packages/core/tests/persistence-migrate.test.ts`, `packages/core/tests/persistence-policy.test.ts`
|
|
20
|
+
|
|
11
21
|
## figcraft
|
|
12
22
|
|
|
13
23
|
© 2026 ArtCraft Team and the FigCraft contributors.
|
|
14
24
|
|
|
15
25
|
- `crates/engine/src/distance.rs` (`between`, `inside`, `to_guide`, `label`) → `packages/core/src/canvas/measure.ts`
|
|
16
26
|
- `crates/engine/src/measure_tests.rs` (test cases re-expressed) → `packages/core/tests/canvas-measure.test.ts`
|
|
27
|
+
- `crates/layout/src/lib.rs` (`layout_frame`, `distribute`, `main_offsets`, `fit_hug_text`, `enforce_limits`, `item_baseline`, `resize_child`, `grid_place`, `resolve_tracks`, `track_content`, `grid_geometry`, `layout_grid`, `apply_constraints`, `layout_page`, `layout_up`, `coherent_sizing`, `clamp_size`) → `packages/core/src/canvas/autoLayout.ts`
|
|
28
|
+
- `crates/layout/src/{tests.rs,behaviour_tests.rs,nested_tests.rs,content_align_tests.rs}` (test cases re-expressed) → `packages/core/tests/canvas-auto-layout.test.ts`
|
|
17
29
|
|
|
18
30
|
## filmcraft
|
|
19
31
|
|
|
@@ -24,6 +36,9 @@ reproduced at the end of this file. Paths are relative to each source repository
|
|
|
24
36
|
- `crates/engine/src/lib.rs` (`Session::execute`, `enabled_for`: re-checking enablement against targets named in params, catching command failures) → `packages/core/src/commands/registry.ts`
|
|
25
37
|
- `crates/engine/src/shortcuts.rs` and `crates/engine/src/shortcuts_tests.rs` (platform-neutral chords, `Mod` resolution per platform, scoped bindings, conflict detection, per-platform conflict test) → `packages/core/src/commands/chords.ts`, `packages/core/src/commands/shortcuts.ts`, `packages/core/tests/commands.test.ts`
|
|
26
38
|
- `crates/automation/src/server.rs` (`command_list` / `command_run` / `command_batch` tools, complete MCP annotations, unknown-argument errors naming the accepted keys) → `packages/core/src/commands/agentTools.ts`, `packages/core/src/commands/registry.ts`
|
|
39
|
+
- `apps/filmcraft-web/src/recovery_policy.rs` (`Policy`, `Action`, `tick` / `written` / `cleaned`: snapshot at most once per interval and revision, retry failed writes, retire only this session's snapshot) → `packages/core/src/persistence/recoveryPolicy.ts`
|
|
40
|
+
- `apps/filmcraft-web/src/recovery.rs` (snapshot-then-dirty-meta writes, recovery check of a dirty meta) → `packages/core/src/persistence/autosave.ts`
|
|
41
|
+
- `apps/filmcraft-web/tests/recovery_policy.rs` (test cases re-expressed) → `packages/core/tests/persistence-policy.test.ts`
|
|
27
42
|
|
|
28
43
|
## gridcraft
|
|
29
44
|
|
|
@@ -33,6 +48,12 @@ reproduced at the end of this file. Paths are relative to each source repository
|
|
|
33
48
|
- `crates/core/src/date.rs` (1900 date-system serials, days in month, civil-day arithmetic) → `packages/core/src/spreadsheet/formula/parse-input.ts`
|
|
34
49
|
- `crates/core/src/value.rs` (Excel comparison, 15-digit number comparison, sort order, number-to-text coercion) → `packages/core/src/spreadsheet/formula/values.ts`
|
|
35
50
|
- Test cases from the files above, re-expressed → `packages/core/tests/spreadsheet-typed-input.test.ts`
|
|
51
|
+
- `crates/numfmt/src/lib.rs`, `parse.rs`, `render.rs`, `decimal.rs`, `builtin.rs` (Excel number format parsing and rendering, 15-digit decimal rounding, General format, `TEXT()`, built-in formats 0–49, parse cache) and `crates/core/src/date.rs` (serial to date) → `packages/core/src/spreadsheet/numfmt/`
|
|
52
|
+
- `crates/numfmt/src/tests.rs` and `crates/numfmt/tests/poi_number_format_approx.rs` (test cases re-expressed) → `packages/core/tests/number-format.test.ts`, `packages/core/tests/fixtures/poi-number-format-approx.ts`
|
|
53
|
+
**Apache POI.** The rows of `packages/core/tests/fixtures/poi-number-format-approx.ts` (format, value and Excel's
|
|
54
|
+
`TEXT()` result) come from Apache POI's `test-data/spreadsheet/NumberFormatApproxTests.xlsx`, as collected in
|
|
55
|
+
GridCraft. Apache POI is © The Apache Software Foundation, licensed under the Apache License, Version 2.0
|
|
56
|
+
(https://www.apache.org/licenses/LICENSE-2.0).
|
|
36
57
|
|
|
37
58
|
## lightcraft
|
|
38
59
|
|
|
@@ -43,6 +64,14 @@ reproduced at the end of this file. Paths are relative to each source repository
|
|
|
43
64
|
- `crates/pipeline/src/tone.rs` (`ToneMap::display` S-curve anchored at 0, grey and 1) → `packages/core/src/image-editor/color.ts`
|
|
44
65
|
- `crates/pipeline/src/local.rs` (relative white balance with neutral-luminance normalisation) → `packages/core/src/image-editor/color.ts`
|
|
45
66
|
- `crates/develop/src/settings.rs` and `docs/process-versions.md` (process version semantics) → `packages/core/src/image-editor/color.ts`
|
|
67
|
+
- `crates/develop/src/controls.rs` (`ControlSpec` table: id, label, section, range, default, step, decimals, track style; clamping) → `packages/core/src/image-editor/controls.ts`
|
|
68
|
+
|
|
69
|
+
## pdfcraft
|
|
70
|
+
|
|
71
|
+
© 2026 ArtCraft Team and the PDFCraft contributors.
|
|
72
|
+
|
|
73
|
+
- `crates/compare/src/lib.rs` (`diff` with common prefix/suffix stripping, `myers` keeping only the −d..d frontier, patience `anchors` with longest increasing subsequence, recursion depth limit, grouping into inserted/deleted/replaced) → `packages/core/src/compare/diff.ts`
|
|
74
|
+
- Test cases from the file above (`long_documents_with_scattered_edits`, `very_different_documents_finish`, `insertions_deletions_and_replacements`), re-expressed → `packages/core/tests/compare-diff.test.ts`
|
|
46
75
|
|
|
47
76
|
## photocraft
|
|
48
77
|
|
|
@@ -50,6 +79,12 @@ reproduced at the end of this file. Paths are relative to each source repository
|
|
|
50
79
|
|
|
51
80
|
- `crates/ops/src/lib.rs` (`History { max_states, max_bytes }`, `trim`, `unique_bytes`) → `packages/core/src/history/HistoryManager.ts`
|
|
52
81
|
- `crates/ui-egui/src/palette.rs` (`fuzzy_score`, `exact_bonus`, `push_recent`) → `packages/core/src/commands/palette.ts`
|
|
82
|
+
- `crates/doc/src/adjust.rs` (`Adjustment` model, `HueRange::neutral` / `weight`, `LevelsChannel` defaults) → `packages/core/src/image-editor/types.ts`, `packages/core/src/image-editor/adjustment-layers.ts`
|
|
83
|
+
- `crates/compose/src/adjust.rs` (`curve_lut`, `levels_q`, `tone_luts_q`, `hue_saturation`, `range_lightness`, `hue_range_tables`, `rgb_to_hsl` / `hsl_to_rgb`, `vibrance_px` / `vibrance_lin`, `color_balance_channel`, `black_white_gray`, `photo_filter_matrix`, `posterize`, Threshold) → `packages/core/src/image-editor/adjustment-layers.ts`
|
|
84
|
+
- `crates/color/src/blend.rs` (separable and non-separable blend modes, hardened `clip_color`, `set_lum`, `composite`) → `packages/core/src/image-editor/blend.ts`
|
|
85
|
+
- `crates/color/src/convert.rs` (`D50`, `SRGB_TO_XYZ_D50`, `XYZ_D50_TO_SRGB`) → `packages/core/src/image-editor/adjustment-layers.ts`
|
|
86
|
+
- `crates/algo/src/histogram.rs` (bounded RGB histogram, transparent exclusion, exact-endpoint clipping) → `packages/core/src/image-editor/histogram.ts`
|
|
87
|
+
- Test cases from `crates/compose/src/adjust.rs`, `crates/compose/src/tests.rs`, `crates/doc/src/adjust.rs`, `crates/color/src/blend.rs` and `crates/algo/src/histogram.rs`, re-expressed → `packages/core/tests/image-editor-adjustment-layers.test.ts`, `packages/core/tests/image-editor-blend-histogram.test.ts`
|
|
53
88
|
|
|
54
89
|
## soundcraft
|
|
55
90
|
|
|
@@ -67,8 +102,24 @@ reproduced at the end of this file. Paths are relative to each source repository
|
|
|
67
102
|
- `crates/pathops/src/fit.rs` (`fit_cubics`, `fit_single`, `fit_single_from`, `generate`, `newton`, `chord_params`, `is_straight`) → `packages/core/src/canvas/curveFit.ts`
|
|
68
103
|
- `crates/pathops/src/edit.rs` (`simplify_with` corner splitting) → `packages/core/src/canvas/curveFit.ts`
|
|
69
104
|
- Test cases from the files above and `crates/pathops/tests/ops.rs`, re-expressed → `packages/core/tests/canvas-resize-snap.test.ts`, `packages/core/tests/canvas-curve-fit.test.ts`
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## wordcraft
|
|
108
|
+
|
|
109
|
+
© 2026 ArtCraft Team and the WordCraft contributors.
|
|
110
|
+
|
|
111
|
+
- `crates/engine/src/cmd/mailings.rs` (`parse_csv` delimiter auto-detection and blank-row skipping, `table_from_rows`, `header_split` padding / truncation and record caps, `address_map` field aliases) → `packages/core/src/grammar/data-source.ts`
|
|
112
|
+
- `crates/formats/src/sheet.rs` (`field_names`: blank headers as `Column N`, repeated headers numbered) → `packages/core/src/grammar/data-source.ts`
|
|
113
|
+
- `crates/engine/src/cmd/mailings.rs` (`condition` numeric-or-text comparison with `= <> < > <= >=`, `finish` one copy per record with SKIPIF and a merge cap) → `packages/core/src/grammar/variables.ts`, `packages/core/src/grammar/merge.ts`
|
|
114
|
+
- Test cases from `crates/engine/src/cmd/mailings.rs` (`csv_parsing`, `merge_end_to_end`, `recipients_from_fields_and_array_rows`, `skipif_drops_matching_records`, `if_rule_previews_its_result`), re-expressed → `packages/core/tests/mail-merge.test.ts`
|
|
115
|
+
- `crates/engine/src/cmd/tools.rs` (`compare`: block-level diff of case-folded / white-space-collapsed paragraph keys, pairing the i-th deleted with the i-th inserted paragraph for word or character diffs, whole insertions/deletions otherwise, `MAX_CHAR_DIFF` fallback, options not honoured; `tokens`, `word_diff`) → `packages/core/src/compare/documents.ts`
|
|
116
|
+
- `crates/engine/src/cmd/fmt_revisions.rs` (`compare_formatting`: equal text compared for formatting changes) → `packages/core/src/compare/documents.ts`
|
|
117
|
+
- Test cases from `crates/engine/src/cmd/tools.rs` (`compare_marks_changes`, `compare_options_ignore_case_and_white_space`), re-expressed → `packages/core/tests/compare-documents.test.ts`
|
|
70
118
|
|
|
71
119
|
---
|
|
120
|
+
- `crates/engine/src/cmd/find_pattern.rs` (`compile`, `special`, `wildcards`, `count`, `class`, `find_code` / `replace_code` / `numeric_code`, `parse_replace`, `expand`) → `packages/core/src/extensions/find-replace/pattern.ts`
|
|
121
|
+
- `crates/engine/src/cmd/edit.rs` (`search_full`: live-text haystack with a position map and cuts at tracked deletions, joining paragraphs only for `^p`, stepping past empty matches, match cap; `replace_all`: last-to-first, tracked path, paragraph splits) → `packages/core/src/extensions/find-replace/search.ts`, `packages/core/src/extensions/find-replace/extension.ts`
|
|
122
|
+
- `crates/engine/src/tests.rs` (`find_replace`, `find_replace_special_codes`, `find_replace_wildcards`, `hostile_find_patterns_are_bounded`, `replace_under_track_changes_skips_deleted_text`, `find_reads_the_text_around_tracked_deletions`, `replace_all_is_tracked`, `replace_all_splits_paragraphs_at_a_paragraph_mark_in_the_replacement`; re-expressed) → `packages/core/tests/find-replace.test.ts`
|
|
72
123
|
|
|
73
124
|
## MIT License
|
|
74
125
|
|
package/dist/agent/index.cjs
CHANGED
|
@@ -5,12 +5,13 @@ var _chunkCPOKFNBGcjs = require('../chunk-CPOKFNBG.cjs');
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
var
|
|
8
|
+
var _chunkXNORY7M6cjs = require('../chunk-XNORY7M6.cjs');
|
|
9
|
+
require('../chunk-D27YFC46.cjs');
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
var
|
|
14
|
+
var _chunkMCSJFWAOcjs = require('../chunk-MCSJFWAO.cjs');
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
|
|
@@ -700,7 +701,7 @@ function createToolRegistry(options) {
|
|
|
700
701
|
var toEditor = (target) => "commands" in target ? target : target.editor;
|
|
701
702
|
function replaceWithText(editor, from, to, text) {
|
|
702
703
|
const { state, view } = editor;
|
|
703
|
-
const tr = /[\r\n]/.test(text) ? state.tr.replace(from, to,
|
|
704
|
+
const tr = /[\r\n]/.test(text) ? state.tr.replace(from, to, _chunkXNORY7M6cjs.plainTextToSlice.call(void 0, text, state.schema)) : text ? state.tr.insertText(text, from, to) : state.tr.delete(from, to);
|
|
704
705
|
view.dispatch(tr.scrollIntoView());
|
|
705
706
|
}
|
|
706
707
|
function selectionInfo(editor) {
|
|
@@ -733,7 +734,7 @@ function createRichTextToolProvider(target, options) {
|
|
|
733
734
|
},
|
|
734
735
|
handler: ({ format = "text" }) => {
|
|
735
736
|
const ed = editor();
|
|
736
|
-
const content = format === "html" ? ed.getHTML() : format === "markdown" ?
|
|
737
|
+
const content = format === "html" ? ed.getHTML() : format === "markdown" ? _chunkXNORY7M6cjs.getEditorMarkdown.call(void 0, ed) : _chunkXNORY7M6cjs.getEditorText.call(void 0, ed);
|
|
737
738
|
return { format, content, selection: selectionInfo(ed), size: ed.state.doc.content.size };
|
|
738
739
|
}
|
|
739
740
|
};
|
|
@@ -815,7 +816,7 @@ var CELL_REF_REGEX = /^\$?([A-Za-z]{1,3})\$?(\d{1,7})$/;
|
|
|
815
816
|
function parseCell(ref) {
|
|
816
817
|
const match = CELL_REF_REGEX.exec(ref.trim());
|
|
817
818
|
if (!match) throw new Error(`Invalid cell reference "${ref}". Use A1 notation, e.g. "B3".`);
|
|
818
|
-
return { col:
|
|
819
|
+
return { col: _chunkMCSJFWAOcjs.colNameToIndex.call(void 0, match[1].toUpperCase()), row: parseInt(match[2], 10) - 1 };
|
|
819
820
|
}
|
|
820
821
|
function createSpreadsheetToolProvider(controller, options) {
|
|
821
822
|
const lookup = (row, col) => {
|
|
@@ -863,14 +864,14 @@ function createSpreadsheetToolProvider(controller, options) {
|
|
|
863
864
|
const cell = controller.getCell(found.rowDef.id, found.colDef.id);
|
|
864
865
|
valueRow.push(_nullishCoalesce(cell.error, () => ( cell.value)));
|
|
865
866
|
rawRow.push(cell.raw);
|
|
866
|
-
const error = _nullishCoalesce(cell.error, () => ( (
|
|
867
|
-
if (error) errors.push({ cell: `${
|
|
867
|
+
const error = _nullishCoalesce(cell.error, () => ( (_chunkMCSJFWAOcjs.isFormulaError.call(void 0, cell.value) ? cell.value : void 0)));
|
|
868
|
+
if (error) errors.push({ cell: `${_chunkMCSJFWAOcjs.indexToColName.call(void 0, c)}${r + 1}`, error });
|
|
868
869
|
}
|
|
869
870
|
values.push(valueRow);
|
|
870
871
|
raw.push(rawRow);
|
|
871
872
|
}
|
|
872
873
|
return {
|
|
873
|
-
range: values.length && values[0].length ? `${
|
|
874
|
+
range: values.length && values[0].length ? `${_chunkMCSJFWAOcjs.indexToColName.call(void 0, c0)}${r0 + 1}:${_chunkMCSJFWAOcjs.indexToColName.call(void 0, c1)}${r1 + 1}` : null,
|
|
874
875
|
values,
|
|
875
876
|
raw,
|
|
876
877
|
errors,
|
package/dist/agent/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/agent/index.cjs","../../src/agent/planner.ts","../../src/agent/tools.ts","../../src/agent/registry.ts","../../src/agent/providers.ts"],"names":[],"mappings":"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","file":"/home/runner/work/editor/editor/packages/core/dist/agent/index.cjs","sourcesContent":[null,"import { ensureLayout, getLayoutedNodes, placeBeside } from '../canvas/layout.js';\nimport type { CanvasEdge, CanvasNode, Rect, Side } from '../canvas/types.js';\nimport type { AgentCommand, AgentStep, EditorSnapshot } from './types.js';\n\n/**\n * Normalizes a placement side from either the planner vocabulary (`right | below | left | above`)\n * or the MCP tool schema vocabulary (`top | right | bottom | left`). Unknown values fall back to `right`.\n */\nexport function normalizeSide(side: string | undefined | null): Side {\n\tswitch (side) {\n\t\tcase 'top':\n\t\tcase 'above':\n\t\t\treturn 'above';\n\t\tcase 'bottom':\n\t\tcase 'below':\n\t\t\treturn 'below';\n\t\tcase 'left':\n\t\t\treturn 'left';\n\t\tcase 'right':\n\t\tdefault:\n\t\t\treturn 'right';\n\t}\n}\n\n/**\n * Plans a sequence of atomic execution steps from a high-level agent command.\n */\nexport function planAgentCommand<\n\tTNode extends CanvasNode = CanvasNode,\n\tTEdge extends CanvasEdge = CanvasEdge\n>(\n\tcommand: AgentCommand<TNode, TEdge>,\n\tsnapshot: EditorSnapshot<TNode, TEdge>,\n\toptions: {\n\t\tdefaultNodeSize?: { width: number; height: number };\n\t\tidGenerator?: () => string;\n\t} = {}\n): AgentStep<TNode, TEdge>[] {\n\tconst defaultNodeSize = options.defaultNodeSize ?? { width: 200, height: 60 };\n\tconst idGen = options.idGenerator ?? (() => `node-${Math.random().toString(36).substring(2, 9)}`);\n\tconst edgeIdGen = () => `edge-${Math.random().toString(36).substring(2, 9)}`;\n\n\tconst nodes = snapshot.canvas?.nodes ?? [];\n\tconst edges = snapshot.canvas?.edges ?? [];\n\n\tswitch (command.type) {\n\t\tcase 'canvas:add_node': {\n\t\t\tlet pos = command.node.position;\n\t\t\t// Only anchor to (and connect from) a node that actually exists.\n\t\t\tconst anchorId =\n\t\t\t\tcommand.nearNodeId && nodes.some((n) => n.id === command.nearNodeId)\n\t\t\t\t\t? command.nearNodeId\n\t\t\t\t\t: undefined;\n\n\t\t\tif (!pos) {\n\t\t\t\tconst rects: Record<string, Rect> = {};\n\t\t\t\tfor (const n of nodes) {\n\t\t\t\t\trects[n.id] = {\n\t\t\t\t\t\tx: n.position.x,\n\t\t\t\t\t\ty: n.position.y,\n\t\t\t\t\t\twidth: n.measured?.width ?? (n as any).width ?? defaultNodeSize.width,\n\t\t\t\t\t\theight: n.measured?.height ?? (n as any).height ?? defaultNodeSize.height\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\tif (anchorId) {\n\t\t\t\t\t// Use collision-aware directional placement\n\t\t\t\t\tpos = placeBeside(rects, defaultNodeSize, normalizeSide(command.side), anchorId);\n\t\t\t\t} else if (nodes.length > 0) {\n\t\t\t\t\t// Default beside the last node\n\t\t\t\t\tconst lastNode = nodes[nodes.length - 1];\n\t\t\t\t\tpos = placeBeside(rects, defaultNodeSize, 'right', lastNode.id);\n\t\t\t\t} else {\n\t\t\t\t\tpos = { x: 100, y: 100 };\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst fullNode: TNode = {\n\t\t\t\t...command.node,\n\t\t\t\tid: command.node.id || idGen(),\n\t\t\t\tposition: pos\n\t\t\t} as TNode;\n\n\t\t\tconst steps: AgentStep<TNode, TEdge>[] = [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:canvas_add_node',\n\t\t\t\t\tnode: fullNode\n\t\t\t\t}\n\t\t\t];\n\n\t\t\t// If attached to nearNode, optionally automatically connect\n\t\t\tif (anchorId) {\n\t\t\t\tsteps.push({\n\t\t\t\t\ttype: 'step:canvas_add_edge',\n\t\t\t\t\tedge: {\n\t\t\t\t\t\tid: edgeIdGen(),\n\t\t\t\t\t\tsource: anchorId,\n\t\t\t\t\t\ttarget: fullNode.id\n\t\t\t\t\t} as TEdge\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn steps;\n\t\t}\n\n\t\tcase 'canvas:remove_nodes': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:canvas_remove_nodes',\n\t\t\t\t\tnodeIds: command.nodeIds\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'canvas:connect': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:canvas_add_edge',\n\t\t\t\t\tedge: {\n\t\t\t\t\t\tid: edgeIdGen(),\n\t\t\t\t\t\tsource: command.source,\n\t\t\t\t\t\ttarget: command.target,\n\t\t\t\t\t\tlabel: command.label\n\t\t\t\t\t} as TEdge\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'canvas:update_node': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:canvas_update_node',\n\t\t\t\t\tid: command.id,\n\t\t\t\t\tdata: command.data\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'canvas:layout': {\n\t\t\tconst dir = command.direction ?? 'LR';\n\t\t\tlet layouted: TNode[];\n\n\t\t\t// Matches the MCP tool schema: `incremental` defaults to true.\n\t\t\tif (command.incremental ?? true) {\n\t\t\t\tconst stored: Record<string, { x: number; y: number }> = {};\n\t\t\t\tfor (const n of nodes) {\n\t\t\t\t\tif (n.position && (n.position.x !== 0 || n.position.y !== 0)) {\n\t\t\t\t\t\tstored[n.id] = { ...n.position };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlayouted = ensureLayout(nodes, edges, {\n\t\t\t\t\tdirection: dir,\n\t\t\t\t\tstoredPositions: stored\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tlayouted = getLayoutedNodes(nodes, edges, undefined, { direction: dir });\n\t\t\t}\n\n\t\t\tconst positions: Record<string, { x: number; y: number }> = {};\n\t\t\tfor (const node of layouted) {\n\t\t\t\tpositions[node.id] = { ...node.position };\n\t\t\t}\n\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:canvas_move_nodes',\n\t\t\t\t\tpositions\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'canvas:select': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:canvas_select',\n\t\t\t\t\tnodeIds: command.nodeIds\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'canvas:highlight': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:canvas_highlight',\n\t\t\t\t\tnodeIds: command.nodeIds,\n\t\t\t\t\tcolor: command.color ?? '#3b82f6',\n\t\t\t\t\tdurationMs: command.durationMs ?? 2000\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'prompt:insert_text': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:prompt_insert',\n\t\t\t\t\ttext: command.text,\n\t\t\t\t\tat: command.at\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'prompt:replace_range': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:prompt_replace',\n\t\t\t\t\tfrom: command.from,\n\t\t\t\t\tto: command.to,\n\t\t\t\t\ttext: command.text\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'prompt:set_variable': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:prompt_set_variable',\n\t\t\t\t\tname: command.name,\n\t\t\t\t\tvalue: command.value\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'prompt:select': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:prompt_select',\n\t\t\t\t\tfrom: command.from,\n\t\t\t\t\tto: command.to\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'choreography:say': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:say',\n\t\t\t\t\ttext: command.text,\n\t\t\t\t\tatNodeId: command.atNodeId,\n\t\t\t\t\tposition: command.position\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'choreography:wait': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:wait',\n\t\t\t\t\tms: command.ms\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'tool:call': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:tool_call',\n\t\t\t\t\tname: command.name,\n\t\t\t\t\targuments: command.arguments ?? {}\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tdefault: {\n\t\t\treturn [];\n\t\t}\n\t}\n}\n\n/**\n * Converts an MCP tool call into a planner command. `edit_canvas` and\n * `edit_prompt` (see `tools.ts`) map to canvas/prompt commands using the MCP\n * schema's vocabulary; every other tool, including dynamic `ToolRegistry`\n * tools, becomes a `tool:call` command. Returns `null` when a static tool's\n * arguments are missing what the operation needs.\n */\nexport function toolCallToAgentCommand<\n\tTNode extends CanvasNode = CanvasNode,\n\tTEdge extends CanvasEdge = CanvasEdge\n>(name: string, args: Record<string, any> = {}): AgentCommand<TNode, TEdge> | null {\n\tconst ids = (value: unknown): string[] | null =>\n\t\tArray.isArray(value) && value.every((v) => typeof v === 'string') ? value : null;\n\tconst num = (value: unknown) => typeof value === 'number' && Number.isFinite(value);\n\n\tif (name === 'edit_canvas') {\n\t\tswitch (args.operation) {\n\t\t\tcase 'add_node':\n\t\t\t\treturn args.node && typeof args.node === 'object'\n\t\t\t\t\t? { type: 'canvas:add_node', node: args.node, nearNodeId: args.nearNodeId, side: args.side }\n\t\t\t\t\t: null;\n\t\t\tcase 'remove_nodes':\n\t\t\t\treturn ids(args.nodeIds) ? { type: 'canvas:remove_nodes', nodeIds: args.nodeIds } : null;\n\t\t\tcase 'connect':\n\t\t\t\treturn typeof args.source === 'string' && typeof args.target === 'string'\n\t\t\t\t\t? { type: 'canvas:connect', source: args.source, target: args.target, label: args.label }\n\t\t\t\t\t: null;\n\t\t\tcase 'update_node': {\n\t\t\t\tconst id = args.node?.id ?? args.id;\n\t\t\t\treturn typeof id === 'string'\n\t\t\t\t\t? { type: 'canvas:update_node', id, data: args.node?.data ?? args.data ?? {} }\n\t\t\t\t\t: null;\n\t\t\t}\n\t\t\tcase 'layout':\n\t\t\t\treturn { type: 'canvas:layout', direction: args.direction, incremental: args.incremental };\n\t\t\tcase 'select':\n\t\t\t\treturn ids(args.nodeIds) ? { type: 'canvas:select', nodeIds: args.nodeIds } : null;\n\t\t\tcase 'highlight':\n\t\t\t\treturn ids(args.nodeIds) ? { type: 'canvas:highlight', nodeIds: args.nodeIds } : null;\n\t\t\tdefault:\n\t\t\t\treturn null;\n\t\t}\n\t}\n\n\tif (name === 'edit_prompt') {\n\t\tswitch (args.operation) {\n\t\t\tcase 'insert_text':\n\t\t\t\treturn typeof args.text === 'string'\n\t\t\t\t\t? { type: 'prompt:insert_text', text: args.text, at: num(args.at) ? args.at : undefined }\n\t\t\t\t\t: null;\n\t\t\tcase 'replace_range':\n\t\t\t\treturn typeof args.text === 'string' && num(args.from) && num(args.to)\n\t\t\t\t\t? { type: 'prompt:replace_range', from: args.from, to: args.to, text: args.text }\n\t\t\t\t\t: null;\n\t\t\tcase 'set_variable':\n\t\t\t\treturn typeof args.name === 'string'\n\t\t\t\t\t? { type: 'prompt:set_variable', name: args.name, value: args.value }\n\t\t\t\t\t: null;\n\t\t\tcase 'select':\n\t\t\t\treturn num(args.from) && num(args.to) ? { type: 'prompt:select', from: args.from, to: args.to } : null;\n\t\t\tdefault:\n\t\t\t\treturn null;\n\t\t}\n\t}\n\n\treturn { type: 'tool:call', name, arguments: args };\n}\n\n/**\n * Plans the steps for an MCP tool call: static editor tools are planned like\n * the equivalent commands, any other tool becomes a `step:tool_call` to be\n * executed with `ToolRegistry.callTool`. Returns `[]` for invalid arguments.\n */\nexport function planToolCall<\n\tTNode extends CanvasNode = CanvasNode,\n\tTEdge extends CanvasEdge = CanvasEdge\n>(\n\tname: string,\n\targs: Record<string, any>,\n\tsnapshot: EditorSnapshot<TNode, TEdge>,\n\toptions?: Parameters<typeof planAgentCommand>[2]\n): AgentStep<TNode, TEdge>[] {\n\tconst command = toolCallToAgentCommand<TNode, TEdge>(name, args);\n\treturn command ? planAgentCommand(command, snapshot, options) : [];\n}\n","/**\n * Standard MCP Tool Definitions for @pixerate/editor.\n * Compatible with the Model Context Protocol (MCP) tool specification schema.\n */\n\nexport const MCP_TOOL_GET_EDITOR_STATE = {\n\tname: 'get_editor_state',\n\ttitle: 'Get editor state',\n\tannotations: {\n\t\ttitle: 'Get editor state',\n\t\treadOnlyHint: true,\n\t\tdestructiveHint: false,\n\t\tidempotentHint: true,\n\t\topenWorldHint: false\n\t},\n\tdescription:\n\t\t'Retrieves the current state of the document, variables, selection, and canvas graph (nodes and edges).',\n\tinputSchema: {\n\t\ttype: 'object',\n\t\tproperties: {\n\t\t\tincludeDocument: {\n\t\t\t\ttype: 'boolean',\n\t\t\t\tdescription: 'Whether to include the prompt document text and variables (default: true).'\n\t\t\t},\n\t\t\tincludeCanvas: {\n\t\t\t\ttype: 'boolean',\n\t\t\t\tdescription: 'Whether to include the canvas nodes and edges (default: true).'\n\t\t\t}\n\t\t}\n\t}\n} as const;\n\nexport const MCP_TOOL_EDIT_CANVAS = {\n\tname: 'edit_canvas',\n\ttitle: 'Edit the canvas',\n\tannotations: {\n\t\ttitle: 'Edit the canvas',\n\t\treadOnlyHint: false,\n\t\tdestructiveHint: true,\n\t\tidempotentHint: false,\n\t\topenWorldHint: false\n\t},\n\tdescription:\n\t\t'Applies high-level structural operations to the visual canvas (adding nodes, connecting edges, organizing layout, or moving elements).',\n\tinputSchema: {\n\t\ttype: 'object',\n\t\tproperties: {\n\t\t\toperation: {\n\t\t\t\ttype: 'string',\n\t\t\t\tenum: [\n\t\t\t\t\t'add_node',\n\t\t\t\t\t'remove_nodes',\n\t\t\t\t\t'connect',\n\t\t\t\t\t'update_node',\n\t\t\t\t\t'layout',\n\t\t\t\t\t'select',\n\t\t\t\t\t'highlight'\n\t\t\t\t],\n\t\t\t\tdescription: 'The canvas operation to execute.'\n\t\t\t},\n\t\t\tnode: {\n\t\t\t\ttype: 'object',\n\t\t\t\tdescription: 'Node definition when operation is add_node. Can omit position to auto-place.'\n\t\t\t},\n\t\t\tnearNodeId: {\n\t\t\t\ttype: 'string',\n\t\t\t\tdescription: 'Anchor node ID to position beside using collision-aware placement.'\n\t\t\t},\n\t\t\tside: {\n\t\t\t\ttype: 'string',\n\t\t\t\tenum: ['top', 'right', 'bottom', 'left'],\n\t\t\t\tdescription: 'Side of the anchor node to place the new node beside.'\n\t\t\t},\n\t\t\tnodeIds: {\n\t\t\t\ttype: 'array',\n\t\t\t\titems: { type: 'string' },\n\t\t\t\tdescription: 'Array of node IDs for remove_nodes, select, or highlight.'\n\t\t\t},\n\t\t\tsource: {\n\t\t\t\ttype: 'string',\n\t\t\t\tdescription: 'Source node ID for connect operation.'\n\t\t\t},\n\t\t\ttarget: {\n\t\t\t\ttype: 'string',\n\t\t\t\tdescription: 'Target node ID for connect operation.'\n\t\t\t},\n\t\t\tlabel: {\n\t\t\t\ttype: 'string',\n\t\t\t\tdescription: 'Optional edge label for connect operation.'\n\t\t\t},\n\t\t\tdirection: {\n\t\t\t\ttype: 'string',\n\t\t\t\tenum: ['LR', 'TB', 'RL', 'BT'],\n\t\t\t\tdescription: 'Layout direction (default LR).'\n\t\t\t},\n\t\t\tincremental: {\n\t\t\t\ttype: 'boolean',\n\t\t\t\tdescription: 'Preserve existing node coordinates and blend layout (default true).'\n\t\t\t}\n\t\t},\n\t\trequired: ['operation']\n\t}\n} as const;\n\nexport const MCP_TOOL_EDIT_PROMPT = {\n\tname: 'edit_prompt',\n\ttitle: 'Edit the prompt',\n\tannotations: {\n\t\ttitle: 'Edit the prompt',\n\t\treadOnlyHint: false,\n\t\tdestructiveHint: true,\n\t\tidempotentHint: false,\n\t\topenWorldHint: false\n\t},\n\tdescription:\n\t\t'Applies edits to the prompt template document, including inserting text, replacing ranges, and setting variables.',\n\tinputSchema: {\n\t\ttype: 'object',\n\t\tproperties: {\n\t\t\toperation: {\n\t\t\t\ttype: 'string',\n\t\t\t\tenum: ['insert_text', 'replace_range', 'set_variable', 'select'],\n\t\t\t\tdescription: 'The prompt operation to execute.'\n\t\t\t},\n\t\t\ttext: {\n\t\t\t\ttype: 'string',\n\t\t\t\tdescription: 'Text to insert or replace.'\n\t\t\t},\n\t\t\tat: {\n\t\t\t\ttype: 'number',\n\t\t\t\tdescription: 'Position to insert text at.'\n\t\t\t},\n\t\t\tfrom: {\n\t\t\t\ttype: 'number',\n\t\t\t\tdescription: 'Start offset for replace_range or select.'\n\t\t\t},\n\t\t\tto: {\n\t\t\t\ttype: 'number',\n\t\t\t\tdescription: 'End offset for replace_range or select.'\n\t\t\t},\n\t\t\tname: {\n\t\t\t\ttype: 'string',\n\t\t\t\tdescription: 'Variable name for set_variable.'\n\t\t\t},\n\t\t\tvalue: {\n\t\t\t\tdescription: 'Variable value for set_variable.'\n\t\t\t}\n\t\t},\n\t\trequired: ['operation']\n\t}\n} as const;\n\nexport const MCP_TOOL_VIEW_EDITOR = {\n\tname: 'view_editor',\n\ttitle: 'View the editor',\n\tannotations: {\n\t\ttitle: 'View the editor',\n\t\treadOnlyHint: true,\n\t\tdestructiveHint: false,\n\t\tidempotentHint: true,\n\t\topenWorldHint: false\n\t},\n\tdescription:\n\t\t'Captures a visual image snapshot (JPEG or PNG) of the editor canvas or viewport for multimodal evaluation. Active agent cursor indicators are automatically excluded from the snapshot.',\n\tinputSchema: {\n\t\ttype: 'object',\n\t\tproperties: {\n\t\t\tscope: {\n\t\t\t\ttype: 'string',\n\t\t\t\tenum: ['viewport', 'canvas'],\n\t\t\t\tdescription: 'Whether to capture only the visible viewport or the full canvas bounding box (default: viewport).'\n\t\t\t},\n\t\t\tformat: {\n\t\t\t\ttype: 'string',\n\t\t\t\tenum: ['jpeg', 'png'],\n\t\t\t\tdescription: 'Image format of the base64 snapshot (default: jpeg).'\n\t\t\t},\n\t\t\tmaxEdge: {\n\t\t\t\ttype: 'integer',\n\t\t\t\tdescription: 'Maximum width or height dimension in pixels (default: 1024).'\n\t\t\t}\n\t\t}\n\t}\n} as const;\n\n/**\n * CSS selectors for ephemeral UI overlays (such as agent cursors, presence tags, and transient bubbles)\n * that should be excluded during canvas image export and visual snapshot generation.\n */\nexport const EXPORT_EXCLUDED_SELECTORS = [\n\t'.pixerate-presence',\n\t'.agent-presence',\n\t'.ursula-presence'\n] as const;\n\nexport const EDITOR_MCP_TOOLS = [\n\tMCP_TOOL_GET_EDITOR_STATE,\n\tMCP_TOOL_EDIT_CANVAS,\n\tMCP_TOOL_EDIT_PROMPT,\n\tMCP_TOOL_VIEW_EDITOR\n] as const;\n\n","import { validateJsonSchema, type JsonSchema, type SchemaValidationError } from './schema.js';\n\n/**\n * MCP-shaped tool definition, as returned by `tools/list`.\n */\nexport interface McpToolDefinition {\n\tname: string;\n\ttitle?: string;\n\tdescription?: string;\n\tinputSchema: JsonSchema & { type: 'object' };\n\toutputSchema?: JsonSchema;\n\tannotations?: {\n\t\ttitle?: string;\n\t\treadOnlyHint?: boolean;\n\t\tdestructiveHint?: boolean;\n\t\tidempotentHint?: boolean;\n\t\topenWorldHint?: boolean;\n\t\t[key: string]: unknown;\n\t};\n}\n\nexport type McpToolContent =\n\t| { type: 'text'; text: string }\n\t| { type: 'image'; data: string; mimeType: string }\n\t| { type: string; [key: string]: unknown };\n\n/** MCP `tools/call` result. */\nexport interface McpToolResult {\n\tcontent: McpToolContent[];\n\tstructuredContent?: Record<string, unknown>;\n\tisError?: boolean;\n}\n\n/** MCP `tools/list` result. */\nexport interface McpListToolsResult {\n\ttools: McpToolDefinition[];\n}\n\nexport interface ToolCallContext {\n\t/** Name the tool was called by (possibly namespaced). */\n\tname: string;\n\t/** Id of the provider that owns the tool. */\n\tproviderId: string;\n\tsignal?: AbortSignal;\n}\n\n/**\n * Handler return values are normalized into an {@link McpToolResult}: a full\n * result (`{ content: [...] }`) is passed through, a string becomes a text\n * block, `undefined` becomes an empty result, and anything else is returned\n * as `structuredContent` (plain objects) plus a JSON text block.\n */\nexport type AgentToolHandler = (args: Record<string, any>, context: ToolCallContext) => unknown | Promise<unknown>;\n\nexport interface AgentTool extends McpToolDefinition {\n\thandler: AgentToolHandler;\n}\n\n/**\n * A source of tools, typically one per mounted editor, spreadsheet or canvas.\n */\nexport interface AgentToolProvider {\n\t/** Unique id within a registry, e.g. `doc-1`. Used as the namespace on collisions. */\n\tid: string;\n\t/** Always prefix this provider's tools with `<namespace>__`. */\n\tnamespace?: string;\n\t/** Tools, or a function returning the current tools (re-read on every `listTools`). */\n\ttools: readonly AgentTool[] | (() => readonly AgentTool[]);\n\t/** Lets the provider signal that its tool list changed; returns an unsubscribe function. */\n\tsubscribe?: (notify: () => void) => () => void;\n}\n\nexport type ToolErrorCode = 'unknown_tool' | 'invalid_arguments' | 'tool_failed';\n\n/** Structured error carried in `structuredContent.error` of a failed call. */\nexport interface ToolCallError {\n\tcode: ToolErrorCode;\n\tmessage: string;\n\ttool: string;\n\tdetails?: SchemaValidationError[];\n}\n\nexport class ToolRegistryError extends Error {\n\treadonly code: 'duplicate_provider' | 'name_collision' | 'invalid_tool';\n\n\tconstructor(code: ToolRegistryError['code'], message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'ToolRegistryError';\n\t\tthis.code = code;\n\t}\n}\n\nexport interface ToolRegistryOptions {\n\t/**\n\t * What to do when two providers contribute the same tool name:\n\t * `namespace` (default) exposes every colliding tool as `<providerId>__<name>`;\n\t * `error` makes `register` throw.\n\t */\n\tonCollision?: 'namespace' | 'error';\n\t/** Coalescing window for `listChanged` notifications in ms (default 0: same tick). */\n\tlistChangedDelayMs?: number;\n\t/** Validate arguments against `inputSchema` before calling handlers (default `true`). */\n\tvalidateArguments?: boolean;\n}\n\n/** Separator between namespace and tool name; valid in MCP and LLM tool-name charsets. */\nexport const TOOL_NAMESPACE_SEPARATOR = '__';\n\nconst TOOL_NAME_REGEX = /^[A-Za-z0-9_.-]{1,128}$/;\nconst sanitizeNamespace = (id: string) => id.replace(/[^A-Za-z0-9_-]/g, '_');\n\ninterface ResolvedTool {\n\tname: string;\n\ttool: AgentTool;\n\tprovider: AgentToolProvider;\n}\n\ninterface ProviderEntry {\n\tprovider: AgentToolProvider;\n\tunsubscribe?: () => void;\n}\n\nfunction readTools(provider: AgentToolProvider): readonly AgentTool[] {\n\treturn typeof provider.tools === 'function' ? provider.tools() : provider.tools;\n}\n\nfunction toDefinition(name: string, tool: AgentTool): McpToolDefinition {\n\tconst { handler: _handler, name: _name, ...rest } = tool;\n\treturn { name, ...rest };\n}\n\nfunction errorResult(error: ToolCallError): McpToolResult {\n\tconst details = error.details?.length\n\t\t? '\\n' + error.details.map((d) => `- ${d.path}: ${d.message}`).join('\\n')\n\t\t: '';\n\treturn {\n\t\tisError: true,\n\t\tcontent: [{ type: 'text', text: `${error.message}${details}` }],\n\t\tstructuredContent: { error: error as unknown as Record<string, unknown> }\n\t};\n}\n\n/** Normalizes a handler's return value into an MCP `tools/call` result. */\nexport function toToolResult(value: unknown): McpToolResult {\n\tif (\n\t\tvalue &&\n\t\ttypeof value === 'object' &&\n\t\tArray.isArray((value as McpToolResult).content)\n\t) {\n\t\treturn value as McpToolResult;\n\t}\n\tif (value === undefined) return { content: [] };\n\tif (typeof value === 'string') return { content: [{ type: 'text', text: value }] };\n\tconst text = JSON.stringify(value);\n\tconst isPlainObject = typeof value === 'object' && value !== null && !Array.isArray(value);\n\treturn {\n\t\tcontent: [{ type: 'text', text: text ?? String(value) }],\n\t\t...(isPlainObject ? { structuredContent: value as Record<string, unknown> } : {})\n\t};\n}\n\n/**\n * Transport-agnostic registry of agent tools contributed by live providers.\n *\n * `listTools()` and `callTool()` return MCP `tools/list` / `tools/call`\n * result shapes, and `onListChanged` fires (coalesced) whenever the exposed\n * tool list changes, mirroring MCP's `notifications/tools/list_changed`, so an\n * MCP server can be a thin wrapper around it.\n */\nexport class ToolRegistry {\n\t/** MCP server capabilities this registry supports. */\n\treadonly capabilities = { tools: { listChanged: true } } as const;\n\n\tprivate readonly entries: ProviderEntry[] = [];\n\tprivate readonly listeners = new Set<(tools: McpToolDefinition[]) => void>();\n\tprivate readonly options: Required<ToolRegistryOptions>;\n\tprivate timer: ReturnType<typeof setTimeout> | null = null;\n\tprivate lastSignature: string;\n\n\tconstructor(options: ToolRegistryOptions = {}) {\n\t\tthis.options = {\n\t\t\tonCollision: options.onCollision ?? 'namespace',\n\t\t\tlistChangedDelayMs: options.listChangedDelayMs ?? 0,\n\t\t\tvalidateArguments: options.validateArguments ?? true\n\t\t};\n\t\tthis.lastSignature = this.signature();\n\t}\n\n\t/**\n\t * Registers a provider and returns a function that unregisters it.\n\t * Throws a {@link ToolRegistryError} for a duplicate provider id, an invalid\n\t * tool name, or (with `onCollision: 'error'`) a tool name collision.\n\t */\n\tregister(provider: AgentToolProvider): () => void {\n\t\tif (this.entries.some((e) => e.provider.id === provider.id)) {\n\t\t\tthrow new ToolRegistryError('duplicate_provider', `Tool provider \"${provider.id}\" is already registered.`);\n\t\t}\n\t\tfor (const tool of readTools(provider)) {\n\t\t\tif (!TOOL_NAME_REGEX.test(tool.name)) {\n\t\t\t\tthrow new ToolRegistryError('invalid_tool', `Invalid tool name \"${tool.name}\" from provider \"${provider.id}\".`);\n\t\t\t}\n\t\t}\n\t\tconst entry: ProviderEntry = { provider };\n\t\tthis.entries.push(entry);\n\t\ttry {\n\t\t\tconst names = this.resolve(this.options.onCollision === 'error').map((t) => t.name);\n\t\t\tconst duplicate = names.find((name, i) => names.indexOf(name) !== i);\n\t\t\tif (duplicate) {\n\t\t\t\t// Only possible with explicit namespaces (or duplicate names within one provider).\n\t\t\t\tthrow new ToolRegistryError('name_collision', `Tool name \"${duplicate}\" is exposed more than once.`);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.entries.pop();\n\t\t\tthrow error;\n\t\t}\n\t\tif (provider.subscribe) {\n\t\t\tentry.unsubscribe = provider.subscribe(() => this.notifyListChanged());\n\t\t}\n\t\tthis.notifyListChanged();\n\t\tlet registered = true;\n\t\treturn () => {\n\t\t\tif (!registered) return;\n\t\t\tregistered = false;\n\t\t\tthis.unregister(provider);\n\t\t};\n\t}\n\n\t/** Removes a provider by id or instance. Returns whether it was registered. */\n\tunregister(providerOrId: string | AgentToolProvider): boolean {\n\t\tconst index = this.entries.findIndex((e) =>\n\t\t\ttypeof providerOrId === 'string' ? e.provider.id === providerOrId : e.provider === providerOrId\n\t\t);\n\t\tif (index === -1) return false;\n\t\tconst [entry] = this.entries.splice(index, 1);\n\t\tentry.unsubscribe?.();\n\t\tthis.notifyListChanged();\n\t\treturn true;\n\t}\n\n\t/** Registered provider ids in registration order. */\n\tproviderIds(): string[] {\n\t\treturn this.entries.map((e) => e.provider.id);\n\t}\n\n\t/**\n\t * Resolves exposed names. Tools are ordered by provider registration, then\n\t * by their order within the provider.\n\t */\n\tprivate resolve(throwOnCollision = false): ResolvedTool[] {\n\t\tconst all: ResolvedTool[] = [];\n\t\tconst counts = new Map<string, number>();\n\t\tfor (const { provider } of this.entries) {\n\t\t\tfor (const tool of readTools(provider)) {\n\t\t\t\tall.push({ name: tool.name, tool, provider });\n\t\t\t\tif (!provider.namespace) counts.set(tool.name, (counts.get(tool.name) ?? 0) + 1);\n\t\t\t}\n\t\t}\n\t\tfor (const item of all) {\n\t\t\tconst prefix = item.provider.namespace ?? ((counts.get(item.name) ?? 0) > 1 ? item.provider.id : undefined);\n\t\t\tif (prefix !== undefined) {\n\t\t\t\tif (throwOnCollision && !item.provider.namespace) {\n\t\t\t\t\tthrow new ToolRegistryError(\n\t\t\t\t\t\t'name_collision',\n\t\t\t\t\t\t`Tool \"${item.name}\" from provider \"${item.provider.id}\" collides with another provider's tool.`\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\titem.name = `${sanitizeNamespace(prefix)}${TOOL_NAMESPACE_SEPARATOR}${item.name}`;\n\t\t\t}\n\t\t}\n\t\treturn all;\n\t}\n\n\t/** MCP `tools/list` result for the current providers. */\n\tlistTools(): McpListToolsResult {\n\t\treturn { tools: this.resolve().map(({ name, tool }) => toDefinition(name, tool)) };\n\t}\n\n\t/** Whether a tool with this exposed name exists. */\n\thasTool(name: string): boolean {\n\t\treturn this.resolve().some((t) => t.name === name);\n\t}\n\n\t/** Looks up a tool definition by exposed name. */\n\tgetTool(name: string): McpToolDefinition | undefined {\n\t\tconst found = this.resolve().find((t) => t.name === name);\n\t\treturn found ? toDefinition(found.name, found.tool) : undefined;\n\t}\n\n\t/**\n\t * Calls a tool by exposed name and returns an MCP `tools/call` result.\n\t * Never rejects: unknown tools, invalid arguments and handler errors come\n\t * back as `isError: true` results with a {@link ToolCallError} in\n\t * `structuredContent.error` (MCP transports may map `unknown_tool` to a\n\t * JSON-RPC `-32602` error instead).\n\t */\n\tasync callTool(name: string, args: Record<string, unknown> = {}, options: { signal?: AbortSignal } = {}): Promise<McpToolResult> {\n\t\tconst found = this.resolve().find((t) => t.name === name);\n\t\tif (!found) {\n\t\t\treturn errorResult({ code: 'unknown_tool', message: `Unknown tool \"${name}\".`, tool: name });\n\t\t}\n\t\tconst input = args ?? {};\n\t\tif (this.options.validateArguments) {\n\t\t\tconst details = validateJsonSchema(input, found.tool.inputSchema);\n\t\t\tif (details.length > 0) {\n\t\t\t\treturn errorResult({\n\t\t\t\t\tcode: 'invalid_arguments',\n\t\t\t\t\tmessage: `Invalid arguments for tool \"${name}\".`,\n\t\t\t\t\ttool: name,\n\t\t\t\t\tdetails\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\tconst value = await found.tool.handler(input as Record<string, any>, {\n\t\t\t\tname,\n\t\t\t\tproviderId: found.provider.id,\n\t\t\t\tsignal: options.signal\n\t\t\t});\n\t\t\treturn toToolResult(value);\n\t\t} catch (error) {\n\t\t\treturn errorResult({\n\t\t\t\tcode: 'tool_failed',\n\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\ttool: name\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Subscribes to tool-list changes. Notifications are coalesced: many\n\t * registrations or provider signals within the delay window produce one\n\t * call, and none if the exposed list ends up unchanged (e.g. a provider\n\t * unmounted and remounted). Returns an unsubscribe function.\n\t */\n\tonListChanged(listener: (tools: McpToolDefinition[]) => void): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\t/** Schedules a coalesced `listChanged` check; providers may call it directly. */\n\tnotifyListChanged(): void {\n\t\tif (this.timer !== null) return;\n\t\tthis.timer = setTimeout(() => this.flush(), this.options.listChangedDelayMs);\n\t}\n\n\t/** Runs a pending `listChanged` check immediately. */\n\tflush(): void {\n\t\tif (this.timer !== null) {\n\t\t\tclearTimeout(this.timer);\n\t\t\tthis.timer = null;\n\t\t}\n\t\tconst signature = this.signature();\n\t\tif (signature === this.lastSignature) return;\n\t\tthis.lastSignature = signature;\n\t\tconst { tools } = this.listTools();\n\t\tfor (const listener of Array.from(this.listeners)) listener(tools);\n\t}\n\n\tprivate signature(): string {\n\t\ttry {\n\t\t\treturn JSON.stringify(this.listTools().tools);\n\t\t} catch {\n\t\t\treturn String(Math.random());\n\t\t}\n\t}\n\n\t/** Unregisters every provider and listener and cancels pending notifications. */\n\tdispose(): void {\n\t\tif (this.timer !== null) clearTimeout(this.timer);\n\t\tthis.timer = null;\n\t\tfor (const entry of this.entries.splice(0)) entry.unsubscribe?.();\n\t\tthis.listeners.clear();\n\t}\n}\n\nexport function createToolRegistry(options?: ToolRegistryOptions): ToolRegistry {\n\treturn new ToolRegistry(options);\n}\n","import type { Editor } from '@tiptap/core';\nimport { plainTextToSlice, getEditorText } from '../serializers/plain-text.js';\nimport { getEditorMarkdown } from '../serializers/markdown.js';\nimport type { SpreadsheetController } from '../spreadsheet/controller.js';\nimport { colNameToIndex, indexToColName, isFormulaError } from '../spreadsheet/formula/evaluator.js';\nimport { planToolCall } from './planner.js';\nimport type { AgentTool, AgentToolProvider } from './registry.js';\nimport {\n\tMCP_TOOL_EDIT_CANVAS,\n\tMCP_TOOL_EDIT_PROMPT,\n\tMCP_TOOL_GET_EDITOR_STATE,\n\tMCP_TOOL_VIEW_EDITOR\n} from './tools.js';\nimport type { AgentStep, EditorSnapshot } from './types.js';\nimport type { CanvasEdge, CanvasNode } from '../canvas/types.js';\n\nexport interface ToolProviderOptions {\n\t/** Provider id (unique per registry). */\n\tid: string;\n\t/** Always prefix tool names with `<namespace>__`. */\n\tnamespace?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Rich-text editor provider\n// ---------------------------------------------------------------------------\n\n/** A TipTap editor, or anything exposing one as `.editor` (e.g. `EditorController`). */\nexport type RichTextToolTarget = Editor | { editor: Editor };\n\nconst toEditor = (target: RichTextToolTarget): Editor =>\n\t'commands' in target ? (target as Editor) : (target as { editor: Editor }).editor;\n\n/** Replaces `from..to` with literal text (HTML is not parsed; newlines split paragraphs). */\nfunction replaceWithText(editor: Editor, from: number, to: number, text: string) {\n\tconst { state, view } = editor;\n\tconst tr = /[\\r\\n]/.test(text)\n\t\t? state.tr.replace(from, to, plainTextToSlice(text, state.schema))\n\t\t: text\n\t\t\t? state.tr.insertText(text, from, to)\n\t\t\t: state.tr.delete(from, to);\n\tview.dispatch(tr.scrollIntoView());\n}\n\nfunction selectionInfo(editor: Editor) {\n\tconst { from, to } = editor.state.selection;\n\treturn { from, to, text: editor.state.doc.textBetween(from, to, '\\n') };\n}\n\n/**\n * Tools for a mounted rich-text editor: `get_text` (always), plus\n * `insert_text` and `replace_selection` while the editor is editable.\n * Positions are ProseMirror document positions (as in `selection.from`).\n * The provider re-announces its tool list when editability changes and\n * exposes no tools once the editor is destroyed.\n */\nexport function createRichTextToolProvider(\n\ttarget: RichTextToolTarget,\n\toptions: ToolProviderOptions\n): AgentToolProvider {\n\tconst editor = () => toEditor(target);\n\n\tconst getText: AgentTool = {\n\t\tname: 'get_text',\n\t\ttitle: 'Get document text',\n\t\tdescription:\n\t\t\t'Returns the document content (plain text by default, or HTML / markdown) and the current selection.',\n\t\tinputSchema: {\n\t\t\ttype: 'object',\n\t\t\tproperties: {\n\t\t\t\tformat: {\n\t\t\t\t\ttype: 'string',\n\t\t\t\t\tenum: ['text', 'html', 'markdown'],\n\t\t\t\t\tdescription: 'Content format (default: text).'\n\t\t\t\t}\n\t\t\t},\n\t\t\tadditionalProperties: false\n\t\t},\n\t\tannotations: {\n\t\t\ttitle: 'Get document text',\n\t\t\treadOnlyHint: true,\n\t\t\tdestructiveHint: false,\n\t\t\tidempotentHint: true,\n\t\t\topenWorldHint: false\n\t\t},\n\t\thandler: ({ format = 'text' }) => {\n\t\t\tconst ed = editor();\n\t\t\tconst content =\n\t\t\t\tformat === 'html' ? ed.getHTML() : format === 'markdown' ? getEditorMarkdown(ed) : getEditorText(ed);\n\t\t\treturn { format, content, selection: selectionInfo(ed), size: ed.state.doc.content.size };\n\t\t}\n\t};\n\n\tconst insertText: AgentTool = {\n\t\tname: 'insert_text',\n\t\ttitle: 'Insert text',\n\t\tdescription:\n\t\t\t'Inserts literal text (newlines start new paragraphs) at a document position, or at the cursor when `at` is omitted.',\n\t\tinputSchema: {\n\t\t\ttype: 'object',\n\t\t\tproperties: {\n\t\t\t\ttext: { type: 'string', description: 'Text to insert.' },\n\t\t\t\tat: { type: 'integer', minimum: 0, description: 'Document position (default: the selection).' }\n\t\t\t},\n\t\t\trequired: ['text'],\n\t\t\tadditionalProperties: false\n\t\t},\n\t\tannotations: {\n\t\t\ttitle: 'Insert text',\n\t\t\treadOnlyHint: false,\n\t\t\tdestructiveHint: false,\n\t\t\tidempotentHint: false,\n\t\t\topenWorldHint: false\n\t\t},\n\t\thandler: ({ text, at }) => {\n\t\t\tconst ed = editor();\n\t\t\tconst size = ed.state.doc.content.size;\n\t\t\tif (at !== undefined && at > size) {\n\t\t\t\tthrow new Error(`Position ${at} is outside the document (size ${size}).`);\n\t\t\t}\n\t\t\tconst { from, to } = ed.state.selection;\n\t\t\treplaceWithText(ed, at ?? from, at ?? to, text);\n\t\t\treturn { inserted: text.length, selection: selectionInfo(ed) };\n\t\t}\n\t};\n\n\tconst replaceSelection: AgentTool = {\n\t\tname: 'replace_selection',\n\t\ttitle: 'Replace selection',\n\t\tdescription: 'Replaces the selected content with literal text (an empty string deletes the selection).',\n\t\tinputSchema: {\n\t\t\ttype: 'object',\n\t\t\tproperties: { text: { type: 'string', description: 'Replacement text.' } },\n\t\t\trequired: ['text'],\n\t\t\tadditionalProperties: false\n\t\t},\n\t\tannotations: {\n\t\t\ttitle: 'Replace selection',\n\t\t\treadOnlyHint: false,\n\t\t\tdestructiveHint: true,\n\t\t\tidempotentHint: false,\n\t\t\topenWorldHint: false\n\t\t},\n\t\thandler: ({ text }) => {\n\t\t\tconst ed = editor();\n\t\t\tconst { from, to } = ed.state.selection;\n\t\t\treplaceWithText(ed, from, to, text);\n\t\t\treturn { replaced: { from, to }, selection: selectionInfo(ed) };\n\t\t}\n\t};\n\n\treturn {\n\t\tid: options.id,\n\t\tnamespace: options.namespace,\n\t\ttools: () => {\n\t\t\tconst ed = editor();\n\t\t\tif (!ed || ed.isDestroyed) return [];\n\t\t\treturn ed.isEditable ? [getText, insertText, replaceSelection] : [getText];\n\t\t},\n\t\tsubscribe: (notify) => {\n\t\t\tconst ed = editor();\n\t\t\t// `setEditable` emits `update`; the registry ignores signals that don't change the list.\n\t\t\ted.on('update', notify);\n\t\t\ted.on('destroy', notify);\n\t\t\treturn () => {\n\t\t\t\ted.off('update', notify);\n\t\t\t\ted.off('destroy', notify);\n\t\t\t};\n\t\t}\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Spreadsheet provider\n// ---------------------------------------------------------------------------\n\nconst CELL_REF_REGEX = /^\\$?([A-Za-z]{1,3})\\$?(\\d{1,7})$/;\n\nfunction parseCell(ref: string): { col: number; row: number } {\n\tconst match = CELL_REF_REGEX.exec(ref.trim());\n\tif (!match) throw new Error(`Invalid cell reference \"${ref}\". Use A1 notation, e.g. \"B3\".`);\n\treturn { col: colNameToIndex(match[1].toUpperCase()), row: parseInt(match[2], 10) - 1 };\n}\n\n/**\n * Tools for a spreadsheet controller: `get_range` (A1 notation, values and raw\n * input) and `set_cell` (raw input; values starting with `=` are formulas).\n */\nexport function createSpreadsheetToolProvider(\n\tcontroller: SpreadsheetController,\n\toptions: ToolProviderOptions\n): AgentToolProvider {\n\tconst lookup = (row: number, col: number) => {\n\t\tconst rowDef = controller.document.rows[row];\n\t\tconst colDef = controller.document.columns[col];\n\t\treturn rowDef && colDef ? { rowDef, colDef } : null;\n\t};\n\n\tconst getRange: AgentTool = {\n\t\tname: 'get_range',\n\t\ttitle: 'Read cells',\n\t\tdescription:\n\t\t\t'Reads a cell range in A1 notation (e.g. \"A1:C5\" or \"B2\") and returns computed values, raw input and errors.',\n\t\tinputSchema: {\n\t\t\ttype: 'object',\n\t\t\tproperties: {\n\t\t\t\trange: { type: 'string', description: 'A1-notation cell or range.' }\n\t\t\t},\n\t\t\trequired: ['range'],\n\t\t\tadditionalProperties: false\n\t\t},\n\t\tannotations: {\n\t\t\ttitle: 'Read cells',\n\t\t\treadOnlyHint: true,\n\t\t\tdestructiveHint: false,\n\t\t\tidempotentHint: true,\n\t\t\topenWorldHint: false\n\t\t},\n\t\thandler: ({ range }) => {\n\t\t\tconst [startRef, endRef = startRef] = String(range).split(':');\n\t\t\tconst start = parseCell(startRef);\n\t\t\tconst end = parseCell(endRef);\n\t\t\tconst rowCount = controller.document.rows.length;\n\t\t\tconst colCount = controller.document.columns.length;\n\t\t\tconst r0 = Math.min(start.row, end.row);\n\t\t\tconst r1 = Math.min(Math.max(start.row, end.row), rowCount - 1);\n\t\t\tconst c0 = Math.min(start.col, end.col);\n\t\t\tconst c1 = Math.min(Math.max(start.col, end.col), colCount - 1);\n\t\t\tconst values: unknown[][] = [];\n\t\t\tconst raw: string[][] = [];\n\t\t\tconst errors: Array<{ cell: string; error: string }> = [];\n\t\t\tfor (let r = r0; r <= r1; r++) {\n\t\t\t\tconst valueRow: unknown[] = [];\n\t\t\t\tconst rawRow: string[] = [];\n\t\t\t\tfor (let c = c0; c <= c1; c++) {\n\t\t\t\t\tconst found = lookup(r, c)!;\n\t\t\t\t\tconst cell = controller.getCell(found.rowDef.id, found.colDef.id);\n\t\t\t\t\tvalueRow.push(cell.error ?? cell.value);\n\t\t\t\t\trawRow.push(cell.raw);\n\t\t\t\t\tconst error = cell.error ?? (isFormulaError(cell.value) ? cell.value : undefined);\n\t\t\t\t\tif (error) errors.push({ cell: `${indexToColName(c)}${r + 1}`, error });\n\t\t\t\t}\n\t\t\t\tvalues.push(valueRow);\n\t\t\t\traw.push(rawRow);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\trange: values.length && values[0].length ? `${indexToColName(c0)}${r0 + 1}:${indexToColName(c1)}${r1 + 1}` : null,\n\t\t\t\tvalues,\n\t\t\t\traw,\n\t\t\t\terrors,\n\t\t\t\tsheet: { rows: rowCount, columns: colCount }\n\t\t\t};\n\t\t}\n\t};\n\n\tconst setCell: AgentTool = {\n\t\tname: 'set_cell',\n\t\ttitle: 'Set a cell',\n\t\tdescription: 'Sets the raw input of one cell (A1 notation). Values starting with \"=\" are formulas.',\n\t\tinputSchema: {\n\t\t\ttype: 'object',\n\t\t\tproperties: {\n\t\t\t\tcell: { type: 'string', description: 'A1-notation cell, e.g. \"B3\".' },\n\t\t\t\tvalue: { type: ['string', 'number', 'boolean'], description: 'Raw cell input.' }\n\t\t\t},\n\t\t\trequired: ['cell', 'value'],\n\t\t\tadditionalProperties: false\n\t\t},\n\t\tannotations: {\n\t\t\ttitle: 'Set a cell',\n\t\t\treadOnlyHint: false,\n\t\t\tdestructiveHint: true,\n\t\t\tidempotentHint: true,\n\t\t\topenWorldHint: false\n\t\t},\n\t\thandler: ({ cell, value }) => {\n\t\t\tconst { row, col } = parseCell(cell);\n\t\t\tconst found = lookup(row, col);\n\t\t\tif (!found) throw new Error(`Cell ${cell} is outside the sheet.`);\n\t\t\tconst { rowDef, colDef } = found;\n\t\t\tif (colDef.readOnly || controller.getCell(rowDef.id, colDef.id).readOnly || colDef.type === 'formula') {\n\t\t\t\tthrow new Error(`Cell ${cell} is read-only.`);\n\t\t\t}\n\t\t\tcontroller.setCellValue(rowDef.id, colDef.id, String(value));\n\t\t\tconst updated = controller.getCell(rowDef.id, colDef.id);\n\t\t\treturn { cell: cell.toUpperCase(), raw: updated.raw, value: updated.error ?? updated.value };\n\t\t}\n\t};\n\n\treturn { id: options.id, namespace: options.namespace, tools: [getRange, setCell] };\n}\n\n// ---------------------------------------------------------------------------\n// Static editor tools (tools.ts) as a provider\n// ---------------------------------------------------------------------------\n\nexport interface EditorMcpToolProviderOptions<\n\tTNode extends CanvasNode = CanvasNode,\n\tTEdge extends CanvasEdge = CanvasEdge\n> extends ToolProviderOptions {\n\t/** Current editor / canvas state. */\n\tgetSnapshot: () => EditorSnapshot<TNode, TEdge>;\n\t/** Applies planned steps (e.g. via your canvas runes / editor controller). */\n\tapplySteps: (steps: AgentStep<TNode, TEdge>[]) => void | Promise<void>;\n\t/** Optional `view_editor` implementation; the tool is only exposed when provided. */\n\tcaptureView?: (args: { scope?: 'viewport' | 'canvas'; format?: 'jpeg' | 'png'; maxEdge?: number }) =>\n\t\t| Promise<{ data: string; mimeType: string }>\n\t\t| { data: string; mimeType: string };\n\t/** Options forwarded to the planner. */\n\tplannerOptions?: Parameters<typeof planToolCall>[3];\n}\n\n/**\n * Exposes the static MCP tools from `tools.ts` (`get_editor_state`,\n * `edit_canvas`, `edit_prompt`, `view_editor`) through a `ToolRegistry`,\n * planning edits with `planToolCall` and handing the steps to `applySteps`.\n */\nexport function createEditorMcpToolProvider<\n\tTNode extends CanvasNode = CanvasNode,\n\tTEdge extends CanvasEdge = CanvasEdge\n>(options: EditorMcpToolProviderOptions<TNode, TEdge>): AgentToolProvider {\n\tconst edit = (name: string) => async (args: Record<string, any>) => {\n\t\tconst steps = planToolCall<TNode, TEdge>(name, args, options.getSnapshot(), options.plannerOptions);\n\t\tif (steps.length === 0) {\n\t\t\tthrow new Error(`Operation \"${args.operation}\" is missing required arguments.`);\n\t\t}\n\t\tawait options.applySteps(steps);\n\t\treturn { applied: steps.length, steps: steps.map((s) => s.type) };\n\t};\n\n\tconst tools: AgentTool[] = [\n\t\t{\n\t\t\t...MCP_TOOL_GET_EDITOR_STATE,\n\t\t\thandler: ({ includeDocument = true, includeCanvas = true }) => {\n\t\t\t\tconst snapshot = options.getSnapshot();\n\t\t\t\treturn {\n\t\t\t\t\t...(includeDocument && snapshot.document ? { document: snapshot.document } : {}),\n\t\t\t\t\t...(includeCanvas && snapshot.canvas ? { canvas: snapshot.canvas } : {})\n\t\t\t\t};\n\t\t\t}\n\t\t},\n\t\t{ ...MCP_TOOL_EDIT_CANVAS, handler: edit(MCP_TOOL_EDIT_CANVAS.name) },\n\t\t{ ...MCP_TOOL_EDIT_PROMPT, handler: edit(MCP_TOOL_EDIT_PROMPT.name) }\n\t] as AgentTool[];\n\n\tif (options.captureView) {\n\t\tconst capture = options.captureView;\n\t\ttools.push({\n\t\t\t...MCP_TOOL_VIEW_EDITOR,\n\t\t\thandler: async (args) => {\n\t\t\t\tconst image = await capture(args);\n\t\t\t\treturn { content: [{ type: 'image', data: image.data, mimeType: image.mimeType }] };\n\t\t\t}\n\t\t} as AgentTool);\n\t}\n\n\treturn { id: options.id, namespace: options.namespace, tools };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/agent/index.cjs","../../src/agent/planner.ts","../../src/agent/tools.ts","../../src/agent/registry.ts","../../src/agent/providers.ts"],"names":[],"mappings":"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","file":"/home/runner/work/editor/editor/packages/core/dist/agent/index.cjs","sourcesContent":[null,"import { ensureLayout, getLayoutedNodes, placeBeside } from '../canvas/layout.js';\nimport type { CanvasEdge, CanvasNode, Rect, Side } from '../canvas/types.js';\nimport type { AgentCommand, AgentStep, EditorSnapshot } from './types.js';\n\n/**\n * Normalizes a placement side from either the planner vocabulary (`right | below | left | above`)\n * or the MCP tool schema vocabulary (`top | right | bottom | left`). Unknown values fall back to `right`.\n */\nexport function normalizeSide(side: string | undefined | null): Side {\n\tswitch (side) {\n\t\tcase 'top':\n\t\tcase 'above':\n\t\t\treturn 'above';\n\t\tcase 'bottom':\n\t\tcase 'below':\n\t\t\treturn 'below';\n\t\tcase 'left':\n\t\t\treturn 'left';\n\t\tcase 'right':\n\t\tdefault:\n\t\t\treturn 'right';\n\t}\n}\n\n/**\n * Plans a sequence of atomic execution steps from a high-level agent command.\n */\nexport function planAgentCommand<\n\tTNode extends CanvasNode = CanvasNode,\n\tTEdge extends CanvasEdge = CanvasEdge\n>(\n\tcommand: AgentCommand<TNode, TEdge>,\n\tsnapshot: EditorSnapshot<TNode, TEdge>,\n\toptions: {\n\t\tdefaultNodeSize?: { width: number; height: number };\n\t\tidGenerator?: () => string;\n\t} = {}\n): AgentStep<TNode, TEdge>[] {\n\tconst defaultNodeSize = options.defaultNodeSize ?? { width: 200, height: 60 };\n\tconst idGen = options.idGenerator ?? (() => `node-${Math.random().toString(36).substring(2, 9)}`);\n\tconst edgeIdGen = () => `edge-${Math.random().toString(36).substring(2, 9)}`;\n\n\tconst nodes = snapshot.canvas?.nodes ?? [];\n\tconst edges = snapshot.canvas?.edges ?? [];\n\n\tswitch (command.type) {\n\t\tcase 'canvas:add_node': {\n\t\t\tlet pos = command.node.position;\n\t\t\t// Only anchor to (and connect from) a node that actually exists.\n\t\t\tconst anchorId =\n\t\t\t\tcommand.nearNodeId && nodes.some((n) => n.id === command.nearNodeId)\n\t\t\t\t\t? command.nearNodeId\n\t\t\t\t\t: undefined;\n\n\t\t\tif (!pos) {\n\t\t\t\tconst rects: Record<string, Rect> = {};\n\t\t\t\tfor (const n of nodes) {\n\t\t\t\t\trects[n.id] = {\n\t\t\t\t\t\tx: n.position.x,\n\t\t\t\t\t\ty: n.position.y,\n\t\t\t\t\t\twidth: n.measured?.width ?? (n as any).width ?? defaultNodeSize.width,\n\t\t\t\t\t\theight: n.measured?.height ?? (n as any).height ?? defaultNodeSize.height\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\tif (anchorId) {\n\t\t\t\t\t// Use collision-aware directional placement\n\t\t\t\t\tpos = placeBeside(rects, defaultNodeSize, normalizeSide(command.side), anchorId);\n\t\t\t\t} else if (nodes.length > 0) {\n\t\t\t\t\t// Default beside the last node\n\t\t\t\t\tconst lastNode = nodes[nodes.length - 1];\n\t\t\t\t\tpos = placeBeside(rects, defaultNodeSize, 'right', lastNode.id);\n\t\t\t\t} else {\n\t\t\t\t\tpos = { x: 100, y: 100 };\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst fullNode: TNode = {\n\t\t\t\t...command.node,\n\t\t\t\tid: command.node.id || idGen(),\n\t\t\t\tposition: pos\n\t\t\t} as TNode;\n\n\t\t\tconst steps: AgentStep<TNode, TEdge>[] = [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:canvas_add_node',\n\t\t\t\t\tnode: fullNode\n\t\t\t\t}\n\t\t\t];\n\n\t\t\t// If attached to nearNode, optionally automatically connect\n\t\t\tif (anchorId) {\n\t\t\t\tsteps.push({\n\t\t\t\t\ttype: 'step:canvas_add_edge',\n\t\t\t\t\tedge: {\n\t\t\t\t\t\tid: edgeIdGen(),\n\t\t\t\t\t\tsource: anchorId,\n\t\t\t\t\t\ttarget: fullNode.id\n\t\t\t\t\t} as TEdge\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn steps;\n\t\t}\n\n\t\tcase 'canvas:remove_nodes': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:canvas_remove_nodes',\n\t\t\t\t\tnodeIds: command.nodeIds\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'canvas:connect': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:canvas_add_edge',\n\t\t\t\t\tedge: {\n\t\t\t\t\t\tid: edgeIdGen(),\n\t\t\t\t\t\tsource: command.source,\n\t\t\t\t\t\ttarget: command.target,\n\t\t\t\t\t\tlabel: command.label\n\t\t\t\t\t} as TEdge\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'canvas:update_node': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:canvas_update_node',\n\t\t\t\t\tid: command.id,\n\t\t\t\t\tdata: command.data\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'canvas:layout': {\n\t\t\tconst dir = command.direction ?? 'LR';\n\t\t\tlet layouted: TNode[];\n\n\t\t\t// Matches the MCP tool schema: `incremental` defaults to true.\n\t\t\tif (command.incremental ?? true) {\n\t\t\t\tconst stored: Record<string, { x: number; y: number }> = {};\n\t\t\t\tfor (const n of nodes) {\n\t\t\t\t\tif (n.position && (n.position.x !== 0 || n.position.y !== 0)) {\n\t\t\t\t\t\tstored[n.id] = { ...n.position };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlayouted = ensureLayout(nodes, edges, {\n\t\t\t\t\tdirection: dir,\n\t\t\t\t\tstoredPositions: stored\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tlayouted = getLayoutedNodes(nodes, edges, undefined, { direction: dir });\n\t\t\t}\n\n\t\t\tconst positions: Record<string, { x: number; y: number }> = {};\n\t\t\tfor (const node of layouted) {\n\t\t\t\tpositions[node.id] = { ...node.position };\n\t\t\t}\n\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:canvas_move_nodes',\n\t\t\t\t\tpositions\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'canvas:select': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:canvas_select',\n\t\t\t\t\tnodeIds: command.nodeIds\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'canvas:highlight': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:canvas_highlight',\n\t\t\t\t\tnodeIds: command.nodeIds,\n\t\t\t\t\tcolor: command.color ?? '#3b82f6',\n\t\t\t\t\tdurationMs: command.durationMs ?? 2000\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'prompt:insert_text': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:prompt_insert',\n\t\t\t\t\ttext: command.text,\n\t\t\t\t\tat: command.at\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'prompt:replace_range': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:prompt_replace',\n\t\t\t\t\tfrom: command.from,\n\t\t\t\t\tto: command.to,\n\t\t\t\t\ttext: command.text\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'prompt:set_variable': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:prompt_set_variable',\n\t\t\t\t\tname: command.name,\n\t\t\t\t\tvalue: command.value\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'prompt:select': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:prompt_select',\n\t\t\t\t\tfrom: command.from,\n\t\t\t\t\tto: command.to\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'choreography:say': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:say',\n\t\t\t\t\ttext: command.text,\n\t\t\t\t\tatNodeId: command.atNodeId,\n\t\t\t\t\tposition: command.position\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'choreography:wait': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:wait',\n\t\t\t\t\tms: command.ms\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tcase 'tool:call': {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\ttype: 'step:tool_call',\n\t\t\t\t\tname: command.name,\n\t\t\t\t\targuments: command.arguments ?? {}\n\t\t\t\t}\n\t\t\t];\n\t\t}\n\n\t\tdefault: {\n\t\t\treturn [];\n\t\t}\n\t}\n}\n\n/**\n * Converts an MCP tool call into a planner command. `edit_canvas` and\n * `edit_prompt` (see `tools.ts`) map to canvas/prompt commands using the MCP\n * schema's vocabulary; every other tool, including dynamic `ToolRegistry`\n * tools, becomes a `tool:call` command. Returns `null` when a static tool's\n * arguments are missing what the operation needs.\n */\nexport function toolCallToAgentCommand<\n\tTNode extends CanvasNode = CanvasNode,\n\tTEdge extends CanvasEdge = CanvasEdge\n>(name: string, args: Record<string, any> = {}): AgentCommand<TNode, TEdge> | null {\n\tconst ids = (value: unknown): string[] | null =>\n\t\tArray.isArray(value) && value.every((v) => typeof v === 'string') ? value : null;\n\tconst num = (value: unknown) => typeof value === 'number' && Number.isFinite(value);\n\n\tif (name === 'edit_canvas') {\n\t\tswitch (args.operation) {\n\t\t\tcase 'add_node':\n\t\t\t\treturn args.node && typeof args.node === 'object'\n\t\t\t\t\t? { type: 'canvas:add_node', node: args.node, nearNodeId: args.nearNodeId, side: args.side }\n\t\t\t\t\t: null;\n\t\t\tcase 'remove_nodes':\n\t\t\t\treturn ids(args.nodeIds) ? { type: 'canvas:remove_nodes', nodeIds: args.nodeIds } : null;\n\t\t\tcase 'connect':\n\t\t\t\treturn typeof args.source === 'string' && typeof args.target === 'string'\n\t\t\t\t\t? { type: 'canvas:connect', source: args.source, target: args.target, label: args.label }\n\t\t\t\t\t: null;\n\t\t\tcase 'update_node': {\n\t\t\t\tconst id = args.node?.id ?? args.id;\n\t\t\t\treturn typeof id === 'string'\n\t\t\t\t\t? { type: 'canvas:update_node', id, data: args.node?.data ?? args.data ?? {} }\n\t\t\t\t\t: null;\n\t\t\t}\n\t\t\tcase 'layout':\n\t\t\t\treturn { type: 'canvas:layout', direction: args.direction, incremental: args.incremental };\n\t\t\tcase 'select':\n\t\t\t\treturn ids(args.nodeIds) ? { type: 'canvas:select', nodeIds: args.nodeIds } : null;\n\t\t\tcase 'highlight':\n\t\t\t\treturn ids(args.nodeIds) ? { type: 'canvas:highlight', nodeIds: args.nodeIds } : null;\n\t\t\tdefault:\n\t\t\t\treturn null;\n\t\t}\n\t}\n\n\tif (name === 'edit_prompt') {\n\t\tswitch (args.operation) {\n\t\t\tcase 'insert_text':\n\t\t\t\treturn typeof args.text === 'string'\n\t\t\t\t\t? { type: 'prompt:insert_text', text: args.text, at: num(args.at) ? args.at : undefined }\n\t\t\t\t\t: null;\n\t\t\tcase 'replace_range':\n\t\t\t\treturn typeof args.text === 'string' && num(args.from) && num(args.to)\n\t\t\t\t\t? { type: 'prompt:replace_range', from: args.from, to: args.to, text: args.text }\n\t\t\t\t\t: null;\n\t\t\tcase 'set_variable':\n\t\t\t\treturn typeof args.name === 'string'\n\t\t\t\t\t? { type: 'prompt:set_variable', name: args.name, value: args.value }\n\t\t\t\t\t: null;\n\t\t\tcase 'select':\n\t\t\t\treturn num(args.from) && num(args.to) ? { type: 'prompt:select', from: args.from, to: args.to } : null;\n\t\t\tdefault:\n\t\t\t\treturn null;\n\t\t}\n\t}\n\n\treturn { type: 'tool:call', name, arguments: args };\n}\n\n/**\n * Plans the steps for an MCP tool call: static editor tools are planned like\n * the equivalent commands, any other tool becomes a `step:tool_call` to be\n * executed with `ToolRegistry.callTool`. Returns `[]` for invalid arguments.\n */\nexport function planToolCall<\n\tTNode extends CanvasNode = CanvasNode,\n\tTEdge extends CanvasEdge = CanvasEdge\n>(\n\tname: string,\n\targs: Record<string, any>,\n\tsnapshot: EditorSnapshot<TNode, TEdge>,\n\toptions?: Parameters<typeof planAgentCommand>[2]\n): AgentStep<TNode, TEdge>[] {\n\tconst command = toolCallToAgentCommand<TNode, TEdge>(name, args);\n\treturn command ? planAgentCommand(command, snapshot, options) : [];\n}\n","/**\n * Standard MCP Tool Definitions for @pixerate/editor.\n * Compatible with the Model Context Protocol (MCP) tool specification schema.\n */\n\nexport const MCP_TOOL_GET_EDITOR_STATE = {\n\tname: 'get_editor_state',\n\ttitle: 'Get editor state',\n\tannotations: {\n\t\ttitle: 'Get editor state',\n\t\treadOnlyHint: true,\n\t\tdestructiveHint: false,\n\t\tidempotentHint: true,\n\t\topenWorldHint: false\n\t},\n\tdescription:\n\t\t'Retrieves the current state of the document, variables, selection, and canvas graph (nodes and edges).',\n\tinputSchema: {\n\t\ttype: 'object',\n\t\tproperties: {\n\t\t\tincludeDocument: {\n\t\t\t\ttype: 'boolean',\n\t\t\t\tdescription: 'Whether to include the prompt document text and variables (default: true).'\n\t\t\t},\n\t\t\tincludeCanvas: {\n\t\t\t\ttype: 'boolean',\n\t\t\t\tdescription: 'Whether to include the canvas nodes and edges (default: true).'\n\t\t\t}\n\t\t}\n\t}\n} as const;\n\nexport const MCP_TOOL_EDIT_CANVAS = {\n\tname: 'edit_canvas',\n\ttitle: 'Edit the canvas',\n\tannotations: {\n\t\ttitle: 'Edit the canvas',\n\t\treadOnlyHint: false,\n\t\tdestructiveHint: true,\n\t\tidempotentHint: false,\n\t\topenWorldHint: false\n\t},\n\tdescription:\n\t\t'Applies high-level structural operations to the visual canvas (adding nodes, connecting edges, organizing layout, or moving elements).',\n\tinputSchema: {\n\t\ttype: 'object',\n\t\tproperties: {\n\t\t\toperation: {\n\t\t\t\ttype: 'string',\n\t\t\t\tenum: [\n\t\t\t\t\t'add_node',\n\t\t\t\t\t'remove_nodes',\n\t\t\t\t\t'connect',\n\t\t\t\t\t'update_node',\n\t\t\t\t\t'layout',\n\t\t\t\t\t'select',\n\t\t\t\t\t'highlight'\n\t\t\t\t],\n\t\t\t\tdescription: 'The canvas operation to execute.'\n\t\t\t},\n\t\t\tnode: {\n\t\t\t\ttype: 'object',\n\t\t\t\tdescription: 'Node definition when operation is add_node. Can omit position to auto-place.'\n\t\t\t},\n\t\t\tnearNodeId: {\n\t\t\t\ttype: 'string',\n\t\t\t\tdescription: 'Anchor node ID to position beside using collision-aware placement.'\n\t\t\t},\n\t\t\tside: {\n\t\t\t\ttype: 'string',\n\t\t\t\tenum: ['top', 'right', 'bottom', 'left'],\n\t\t\t\tdescription: 'Side of the anchor node to place the new node beside.'\n\t\t\t},\n\t\t\tnodeIds: {\n\t\t\t\ttype: 'array',\n\t\t\t\titems: { type: 'string' },\n\t\t\t\tdescription: 'Array of node IDs for remove_nodes, select, or highlight.'\n\t\t\t},\n\t\t\tsource: {\n\t\t\t\ttype: 'string',\n\t\t\t\tdescription: 'Source node ID for connect operation.'\n\t\t\t},\n\t\t\ttarget: {\n\t\t\t\ttype: 'string',\n\t\t\t\tdescription: 'Target node ID for connect operation.'\n\t\t\t},\n\t\t\tlabel: {\n\t\t\t\ttype: 'string',\n\t\t\t\tdescription: 'Optional edge label for connect operation.'\n\t\t\t},\n\t\t\tdirection: {\n\t\t\t\ttype: 'string',\n\t\t\t\tenum: ['LR', 'TB', 'RL', 'BT'],\n\t\t\t\tdescription: 'Layout direction (default LR).'\n\t\t\t},\n\t\t\tincremental: {\n\t\t\t\ttype: 'boolean',\n\t\t\t\tdescription: 'Preserve existing node coordinates and blend layout (default true).'\n\t\t\t}\n\t\t},\n\t\trequired: ['operation']\n\t}\n} as const;\n\nexport const MCP_TOOL_EDIT_PROMPT = {\n\tname: 'edit_prompt',\n\ttitle: 'Edit the prompt',\n\tannotations: {\n\t\ttitle: 'Edit the prompt',\n\t\treadOnlyHint: false,\n\t\tdestructiveHint: true,\n\t\tidempotentHint: false,\n\t\topenWorldHint: false\n\t},\n\tdescription:\n\t\t'Applies edits to the prompt template document, including inserting text, replacing ranges, and setting variables.',\n\tinputSchema: {\n\t\ttype: 'object',\n\t\tproperties: {\n\t\t\toperation: {\n\t\t\t\ttype: 'string',\n\t\t\t\tenum: ['insert_text', 'replace_range', 'set_variable', 'select'],\n\t\t\t\tdescription: 'The prompt operation to execute.'\n\t\t\t},\n\t\t\ttext: {\n\t\t\t\ttype: 'string',\n\t\t\t\tdescription: 'Text to insert or replace.'\n\t\t\t},\n\t\t\tat: {\n\t\t\t\ttype: 'number',\n\t\t\t\tdescription: 'Position to insert text at.'\n\t\t\t},\n\t\t\tfrom: {\n\t\t\t\ttype: 'number',\n\t\t\t\tdescription: 'Start offset for replace_range or select.'\n\t\t\t},\n\t\t\tto: {\n\t\t\t\ttype: 'number',\n\t\t\t\tdescription: 'End offset for replace_range or select.'\n\t\t\t},\n\t\t\tname: {\n\t\t\t\ttype: 'string',\n\t\t\t\tdescription: 'Variable name for set_variable.'\n\t\t\t},\n\t\t\tvalue: {\n\t\t\t\tdescription: 'Variable value for set_variable.'\n\t\t\t}\n\t\t},\n\t\trequired: ['operation']\n\t}\n} as const;\n\nexport const MCP_TOOL_VIEW_EDITOR = {\n\tname: 'view_editor',\n\ttitle: 'View the editor',\n\tannotations: {\n\t\ttitle: 'View the editor',\n\t\treadOnlyHint: true,\n\t\tdestructiveHint: false,\n\t\tidempotentHint: true,\n\t\topenWorldHint: false\n\t},\n\tdescription:\n\t\t'Captures a visual image snapshot (JPEG or PNG) of the editor canvas or viewport for multimodal evaluation. Active agent cursor indicators are automatically excluded from the snapshot.',\n\tinputSchema: {\n\t\ttype: 'object',\n\t\tproperties: {\n\t\t\tscope: {\n\t\t\t\ttype: 'string',\n\t\t\t\tenum: ['viewport', 'canvas'],\n\t\t\t\tdescription: 'Whether to capture only the visible viewport or the full canvas bounding box (default: viewport).'\n\t\t\t},\n\t\t\tformat: {\n\t\t\t\ttype: 'string',\n\t\t\t\tenum: ['jpeg', 'png'],\n\t\t\t\tdescription: 'Image format of the base64 snapshot (default: jpeg).'\n\t\t\t},\n\t\t\tmaxEdge: {\n\t\t\t\ttype: 'integer',\n\t\t\t\tdescription: 'Maximum width or height dimension in pixels (default: 1024).'\n\t\t\t}\n\t\t}\n\t}\n} as const;\n\n/**\n * CSS selectors for ephemeral UI overlays (such as agent cursors, presence tags, and transient bubbles)\n * that should be excluded during canvas image export and visual snapshot generation.\n */\nexport const EXPORT_EXCLUDED_SELECTORS = [\n\t'.pixerate-presence',\n\t'.agent-presence',\n\t'.ursula-presence'\n] as const;\n\nexport const EDITOR_MCP_TOOLS = [\n\tMCP_TOOL_GET_EDITOR_STATE,\n\tMCP_TOOL_EDIT_CANVAS,\n\tMCP_TOOL_EDIT_PROMPT,\n\tMCP_TOOL_VIEW_EDITOR\n] as const;\n\n","import { validateJsonSchema, type JsonSchema, type SchemaValidationError } from './schema.js';\n\n/**\n * MCP-shaped tool definition, as returned by `tools/list`.\n */\nexport interface McpToolDefinition {\n\tname: string;\n\ttitle?: string;\n\tdescription?: string;\n\tinputSchema: JsonSchema & { type: 'object' };\n\toutputSchema?: JsonSchema;\n\tannotations?: {\n\t\ttitle?: string;\n\t\treadOnlyHint?: boolean;\n\t\tdestructiveHint?: boolean;\n\t\tidempotentHint?: boolean;\n\t\topenWorldHint?: boolean;\n\t\t[key: string]: unknown;\n\t};\n}\n\nexport type McpToolContent =\n\t| { type: 'text'; text: string }\n\t| { type: 'image'; data: string; mimeType: string }\n\t| { type: string; [key: string]: unknown };\n\n/** MCP `tools/call` result. */\nexport interface McpToolResult {\n\tcontent: McpToolContent[];\n\tstructuredContent?: Record<string, unknown>;\n\tisError?: boolean;\n}\n\n/** MCP `tools/list` result. */\nexport interface McpListToolsResult {\n\ttools: McpToolDefinition[];\n}\n\nexport interface ToolCallContext {\n\t/** Name the tool was called by (possibly namespaced). */\n\tname: string;\n\t/** Id of the provider that owns the tool. */\n\tproviderId: string;\n\tsignal?: AbortSignal;\n}\n\n/**\n * Handler return values are normalized into an {@link McpToolResult}: a full\n * result (`{ content: [...] }`) is passed through, a string becomes a text\n * block, `undefined` becomes an empty result, and anything else is returned\n * as `structuredContent` (plain objects) plus a JSON text block.\n */\nexport type AgentToolHandler = (args: Record<string, any>, context: ToolCallContext) => unknown | Promise<unknown>;\n\nexport interface AgentTool extends McpToolDefinition {\n\thandler: AgentToolHandler;\n}\n\n/**\n * A source of tools, typically one per mounted editor, spreadsheet or canvas.\n */\nexport interface AgentToolProvider {\n\t/** Unique id within a registry, e.g. `doc-1`. Used as the namespace on collisions. */\n\tid: string;\n\t/** Always prefix this provider's tools with `<namespace>__`. */\n\tnamespace?: string;\n\t/** Tools, or a function returning the current tools (re-read on every `listTools`). */\n\ttools: readonly AgentTool[] | (() => readonly AgentTool[]);\n\t/** Lets the provider signal that its tool list changed; returns an unsubscribe function. */\n\tsubscribe?: (notify: () => void) => () => void;\n}\n\nexport type ToolErrorCode = 'unknown_tool' | 'invalid_arguments' | 'tool_failed';\n\n/** Structured error carried in `structuredContent.error` of a failed call. */\nexport interface ToolCallError {\n\tcode: ToolErrorCode;\n\tmessage: string;\n\ttool: string;\n\tdetails?: SchemaValidationError[];\n}\n\nexport class ToolRegistryError extends Error {\n\treadonly code: 'duplicate_provider' | 'name_collision' | 'invalid_tool';\n\n\tconstructor(code: ToolRegistryError['code'], message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'ToolRegistryError';\n\t\tthis.code = code;\n\t}\n}\n\nexport interface ToolRegistryOptions {\n\t/**\n\t * What to do when two providers contribute the same tool name:\n\t * `namespace` (default) exposes every colliding tool as `<providerId>__<name>`;\n\t * `error` makes `register` throw.\n\t */\n\tonCollision?: 'namespace' | 'error';\n\t/** Coalescing window for `listChanged` notifications in ms (default 0: same tick). */\n\tlistChangedDelayMs?: number;\n\t/** Validate arguments against `inputSchema` before calling handlers (default `true`). */\n\tvalidateArguments?: boolean;\n}\n\n/** Separator between namespace and tool name; valid in MCP and LLM tool-name charsets. */\nexport const TOOL_NAMESPACE_SEPARATOR = '__';\n\nconst TOOL_NAME_REGEX = /^[A-Za-z0-9_.-]{1,128}$/;\nconst sanitizeNamespace = (id: string) => id.replace(/[^A-Za-z0-9_-]/g, '_');\n\ninterface ResolvedTool {\n\tname: string;\n\ttool: AgentTool;\n\tprovider: AgentToolProvider;\n}\n\ninterface ProviderEntry {\n\tprovider: AgentToolProvider;\n\tunsubscribe?: () => void;\n}\n\nfunction readTools(provider: AgentToolProvider): readonly AgentTool[] {\n\treturn typeof provider.tools === 'function' ? provider.tools() : provider.tools;\n}\n\nfunction toDefinition(name: string, tool: AgentTool): McpToolDefinition {\n\tconst { handler: _handler, name: _name, ...rest } = tool;\n\treturn { name, ...rest };\n}\n\nfunction errorResult(error: ToolCallError): McpToolResult {\n\tconst details = error.details?.length\n\t\t? '\\n' + error.details.map((d) => `- ${d.path}: ${d.message}`).join('\\n')\n\t\t: '';\n\treturn {\n\t\tisError: true,\n\t\tcontent: [{ type: 'text', text: `${error.message}${details}` }],\n\t\tstructuredContent: { error: error as unknown as Record<string, unknown> }\n\t};\n}\n\n/** Normalizes a handler's return value into an MCP `tools/call` result. */\nexport function toToolResult(value: unknown): McpToolResult {\n\tif (\n\t\tvalue &&\n\t\ttypeof value === 'object' &&\n\t\tArray.isArray((value as McpToolResult).content)\n\t) {\n\t\treturn value as McpToolResult;\n\t}\n\tif (value === undefined) return { content: [] };\n\tif (typeof value === 'string') return { content: [{ type: 'text', text: value }] };\n\tconst text = JSON.stringify(value);\n\tconst isPlainObject = typeof value === 'object' && value !== null && !Array.isArray(value);\n\treturn {\n\t\tcontent: [{ type: 'text', text: text ?? String(value) }],\n\t\t...(isPlainObject ? { structuredContent: value as Record<string, unknown> } : {})\n\t};\n}\n\n/**\n * Transport-agnostic registry of agent tools contributed by live providers.\n *\n * `listTools()` and `callTool()` return MCP `tools/list` / `tools/call`\n * result shapes, and `onListChanged` fires (coalesced) whenever the exposed\n * tool list changes, mirroring MCP's `notifications/tools/list_changed`, so an\n * MCP server can be a thin wrapper around it.\n */\nexport class ToolRegistry {\n\t/** MCP server capabilities this registry supports. */\n\treadonly capabilities = { tools: { listChanged: true } } as const;\n\n\tprivate readonly entries: ProviderEntry[] = [];\n\tprivate readonly listeners = new Set<(tools: McpToolDefinition[]) => void>();\n\tprivate readonly options: Required<ToolRegistryOptions>;\n\tprivate timer: ReturnType<typeof setTimeout> | null = null;\n\tprivate lastSignature: string;\n\n\tconstructor(options: ToolRegistryOptions = {}) {\n\t\tthis.options = {\n\t\t\tonCollision: options.onCollision ?? 'namespace',\n\t\t\tlistChangedDelayMs: options.listChangedDelayMs ?? 0,\n\t\t\tvalidateArguments: options.validateArguments ?? true\n\t\t};\n\t\tthis.lastSignature = this.signature();\n\t}\n\n\t/**\n\t * Registers a provider and returns a function that unregisters it.\n\t * Throws a {@link ToolRegistryError} for a duplicate provider id, an invalid\n\t * tool name, or (with `onCollision: 'error'`) a tool name collision.\n\t */\n\tregister(provider: AgentToolProvider): () => void {\n\t\tif (this.entries.some((e) => e.provider.id === provider.id)) {\n\t\t\tthrow new ToolRegistryError('duplicate_provider', `Tool provider \"${provider.id}\" is already registered.`);\n\t\t}\n\t\tfor (const tool of readTools(provider)) {\n\t\t\tif (!TOOL_NAME_REGEX.test(tool.name)) {\n\t\t\t\tthrow new ToolRegistryError('invalid_tool', `Invalid tool name \"${tool.name}\" from provider \"${provider.id}\".`);\n\t\t\t}\n\t\t}\n\t\tconst entry: ProviderEntry = { provider };\n\t\tthis.entries.push(entry);\n\t\ttry {\n\t\t\tconst names = this.resolve(this.options.onCollision === 'error').map((t) => t.name);\n\t\t\tconst duplicate = names.find((name, i) => names.indexOf(name) !== i);\n\t\t\tif (duplicate) {\n\t\t\t\t// Only possible with explicit namespaces (or duplicate names within one provider).\n\t\t\t\tthrow new ToolRegistryError('name_collision', `Tool name \"${duplicate}\" is exposed more than once.`);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.entries.pop();\n\t\t\tthrow error;\n\t\t}\n\t\tif (provider.subscribe) {\n\t\t\tentry.unsubscribe = provider.subscribe(() => this.notifyListChanged());\n\t\t}\n\t\tthis.notifyListChanged();\n\t\tlet registered = true;\n\t\treturn () => {\n\t\t\tif (!registered) return;\n\t\t\tregistered = false;\n\t\t\tthis.unregister(provider);\n\t\t};\n\t}\n\n\t/** Removes a provider by id or instance. Returns whether it was registered. */\n\tunregister(providerOrId: string | AgentToolProvider): boolean {\n\t\tconst index = this.entries.findIndex((e) =>\n\t\t\ttypeof providerOrId === 'string' ? e.provider.id === providerOrId : e.provider === providerOrId\n\t\t);\n\t\tif (index === -1) return false;\n\t\tconst [entry] = this.entries.splice(index, 1);\n\t\tentry.unsubscribe?.();\n\t\tthis.notifyListChanged();\n\t\treturn true;\n\t}\n\n\t/** Registered provider ids in registration order. */\n\tproviderIds(): string[] {\n\t\treturn this.entries.map((e) => e.provider.id);\n\t}\n\n\t/**\n\t * Resolves exposed names. Tools are ordered by provider registration, then\n\t * by their order within the provider.\n\t */\n\tprivate resolve(throwOnCollision = false): ResolvedTool[] {\n\t\tconst all: ResolvedTool[] = [];\n\t\tconst counts = new Map<string, number>();\n\t\tfor (const { provider } of this.entries) {\n\t\t\tfor (const tool of readTools(provider)) {\n\t\t\t\tall.push({ name: tool.name, tool, provider });\n\t\t\t\tif (!provider.namespace) counts.set(tool.name, (counts.get(tool.name) ?? 0) + 1);\n\t\t\t}\n\t\t}\n\t\tfor (const item of all) {\n\t\t\tconst prefix = item.provider.namespace ?? ((counts.get(item.name) ?? 0) > 1 ? item.provider.id : undefined);\n\t\t\tif (prefix !== undefined) {\n\t\t\t\tif (throwOnCollision && !item.provider.namespace) {\n\t\t\t\t\tthrow new ToolRegistryError(\n\t\t\t\t\t\t'name_collision',\n\t\t\t\t\t\t`Tool \"${item.name}\" from provider \"${item.provider.id}\" collides with another provider's tool.`\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\titem.name = `${sanitizeNamespace(prefix)}${TOOL_NAMESPACE_SEPARATOR}${item.name}`;\n\t\t\t}\n\t\t}\n\t\treturn all;\n\t}\n\n\t/** MCP `tools/list` result for the current providers. */\n\tlistTools(): McpListToolsResult {\n\t\treturn { tools: this.resolve().map(({ name, tool }) => toDefinition(name, tool)) };\n\t}\n\n\t/** Whether a tool with this exposed name exists. */\n\thasTool(name: string): boolean {\n\t\treturn this.resolve().some((t) => t.name === name);\n\t}\n\n\t/** Looks up a tool definition by exposed name. */\n\tgetTool(name: string): McpToolDefinition | undefined {\n\t\tconst found = this.resolve().find((t) => t.name === name);\n\t\treturn found ? toDefinition(found.name, found.tool) : undefined;\n\t}\n\n\t/**\n\t * Calls a tool by exposed name and returns an MCP `tools/call` result.\n\t * Never rejects: unknown tools, invalid arguments and handler errors come\n\t * back as `isError: true` results with a {@link ToolCallError} in\n\t * `structuredContent.error` (MCP transports may map `unknown_tool` to a\n\t * JSON-RPC `-32602` error instead).\n\t */\n\tasync callTool(name: string, args: Record<string, unknown> = {}, options: { signal?: AbortSignal } = {}): Promise<McpToolResult> {\n\t\tconst found = this.resolve().find((t) => t.name === name);\n\t\tif (!found) {\n\t\t\treturn errorResult({ code: 'unknown_tool', message: `Unknown tool \"${name}\".`, tool: name });\n\t\t}\n\t\tconst input = args ?? {};\n\t\tif (this.options.validateArguments) {\n\t\t\tconst details = validateJsonSchema(input, found.tool.inputSchema);\n\t\t\tif (details.length > 0) {\n\t\t\t\treturn errorResult({\n\t\t\t\t\tcode: 'invalid_arguments',\n\t\t\t\t\tmessage: `Invalid arguments for tool \"${name}\".`,\n\t\t\t\t\ttool: name,\n\t\t\t\t\tdetails\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\tconst value = await found.tool.handler(input as Record<string, any>, {\n\t\t\t\tname,\n\t\t\t\tproviderId: found.provider.id,\n\t\t\t\tsignal: options.signal\n\t\t\t});\n\t\t\treturn toToolResult(value);\n\t\t} catch (error) {\n\t\t\treturn errorResult({\n\t\t\t\tcode: 'tool_failed',\n\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\ttool: name\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Subscribes to tool-list changes. Notifications are coalesced: many\n\t * registrations or provider signals within the delay window produce one\n\t * call, and none if the exposed list ends up unchanged (e.g. a provider\n\t * unmounted and remounted). Returns an unsubscribe function.\n\t */\n\tonListChanged(listener: (tools: McpToolDefinition[]) => void): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\t/** Schedules a coalesced `listChanged` check; providers may call it directly. */\n\tnotifyListChanged(): void {\n\t\tif (this.timer !== null) return;\n\t\tthis.timer = setTimeout(() => this.flush(), this.options.listChangedDelayMs);\n\t}\n\n\t/** Runs a pending `listChanged` check immediately. */\n\tflush(): void {\n\t\tif (this.timer !== null) {\n\t\t\tclearTimeout(this.timer);\n\t\t\tthis.timer = null;\n\t\t}\n\t\tconst signature = this.signature();\n\t\tif (signature === this.lastSignature) return;\n\t\tthis.lastSignature = signature;\n\t\tconst { tools } = this.listTools();\n\t\tfor (const listener of Array.from(this.listeners)) listener(tools);\n\t}\n\n\tprivate signature(): string {\n\t\ttry {\n\t\t\treturn JSON.stringify(this.listTools().tools);\n\t\t} catch {\n\t\t\treturn String(Math.random());\n\t\t}\n\t}\n\n\t/** Unregisters every provider and listener and cancels pending notifications. */\n\tdispose(): void {\n\t\tif (this.timer !== null) clearTimeout(this.timer);\n\t\tthis.timer = null;\n\t\tfor (const entry of this.entries.splice(0)) entry.unsubscribe?.();\n\t\tthis.listeners.clear();\n\t}\n}\n\nexport function createToolRegistry(options?: ToolRegistryOptions): ToolRegistry {\n\treturn new ToolRegistry(options);\n}\n","import type { Editor } from '@tiptap/core';\nimport { plainTextToSlice, getEditorText } from '../serializers/plain-text.js';\nimport { getEditorMarkdown } from '../serializers/markdown.js';\nimport type { SpreadsheetController } from '../spreadsheet/controller.js';\nimport { colNameToIndex, indexToColName, isFormulaError } from '../spreadsheet/formula/evaluator.js';\nimport { planToolCall } from './planner.js';\nimport type { AgentTool, AgentToolProvider } from './registry.js';\nimport {\n\tMCP_TOOL_EDIT_CANVAS,\n\tMCP_TOOL_EDIT_PROMPT,\n\tMCP_TOOL_GET_EDITOR_STATE,\n\tMCP_TOOL_VIEW_EDITOR\n} from './tools.js';\nimport type { AgentStep, EditorSnapshot } from './types.js';\nimport type { CanvasEdge, CanvasNode } from '../canvas/types.js';\n\nexport interface ToolProviderOptions {\n\t/** Provider id (unique per registry). */\n\tid: string;\n\t/** Always prefix tool names with `<namespace>__`. */\n\tnamespace?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Rich-text editor provider\n// ---------------------------------------------------------------------------\n\n/** A TipTap editor, or anything exposing one as `.editor` (e.g. `EditorController`). */\nexport type RichTextToolTarget = Editor | { editor: Editor };\n\nconst toEditor = (target: RichTextToolTarget): Editor =>\n\t'commands' in target ? (target as Editor) : (target as { editor: Editor }).editor;\n\n/** Replaces `from..to` with literal text (HTML is not parsed; newlines split paragraphs). */\nfunction replaceWithText(editor: Editor, from: number, to: number, text: string) {\n\tconst { state, view } = editor;\n\tconst tr = /[\\r\\n]/.test(text)\n\t\t? state.tr.replace(from, to, plainTextToSlice(text, state.schema))\n\t\t: text\n\t\t\t? state.tr.insertText(text, from, to)\n\t\t\t: state.tr.delete(from, to);\n\tview.dispatch(tr.scrollIntoView());\n}\n\nfunction selectionInfo(editor: Editor) {\n\tconst { from, to } = editor.state.selection;\n\treturn { from, to, text: editor.state.doc.textBetween(from, to, '\\n') };\n}\n\n/**\n * Tools for a mounted rich-text editor: `get_text` (always), plus\n * `insert_text` and `replace_selection` while the editor is editable.\n * Positions are ProseMirror document positions (as in `selection.from`).\n * The provider re-announces its tool list when editability changes and\n * exposes no tools once the editor is destroyed.\n */\nexport function createRichTextToolProvider(\n\ttarget: RichTextToolTarget,\n\toptions: ToolProviderOptions\n): AgentToolProvider {\n\tconst editor = () => toEditor(target);\n\n\tconst getText: AgentTool = {\n\t\tname: 'get_text',\n\t\ttitle: 'Get document text',\n\t\tdescription:\n\t\t\t'Returns the document content (plain text by default, or HTML / markdown) and the current selection.',\n\t\tinputSchema: {\n\t\t\ttype: 'object',\n\t\t\tproperties: {\n\t\t\t\tformat: {\n\t\t\t\t\ttype: 'string',\n\t\t\t\t\tenum: ['text', 'html', 'markdown'],\n\t\t\t\t\tdescription: 'Content format (default: text).'\n\t\t\t\t}\n\t\t\t},\n\t\t\tadditionalProperties: false\n\t\t},\n\t\tannotations: {\n\t\t\ttitle: 'Get document text',\n\t\t\treadOnlyHint: true,\n\t\t\tdestructiveHint: false,\n\t\t\tidempotentHint: true,\n\t\t\topenWorldHint: false\n\t\t},\n\t\thandler: ({ format = 'text' }) => {\n\t\t\tconst ed = editor();\n\t\t\tconst content =\n\t\t\t\tformat === 'html' ? ed.getHTML() : format === 'markdown' ? getEditorMarkdown(ed) : getEditorText(ed);\n\t\t\treturn { format, content, selection: selectionInfo(ed), size: ed.state.doc.content.size };\n\t\t}\n\t};\n\n\tconst insertText: AgentTool = {\n\t\tname: 'insert_text',\n\t\ttitle: 'Insert text',\n\t\tdescription:\n\t\t\t'Inserts literal text (newlines start new paragraphs) at a document position, or at the cursor when `at` is omitted.',\n\t\tinputSchema: {\n\t\t\ttype: 'object',\n\t\t\tproperties: {\n\t\t\t\ttext: { type: 'string', description: 'Text to insert.' },\n\t\t\t\tat: { type: 'integer', minimum: 0, description: 'Document position (default: the selection).' }\n\t\t\t},\n\t\t\trequired: ['text'],\n\t\t\tadditionalProperties: false\n\t\t},\n\t\tannotations: {\n\t\t\ttitle: 'Insert text',\n\t\t\treadOnlyHint: false,\n\t\t\tdestructiveHint: false,\n\t\t\tidempotentHint: false,\n\t\t\topenWorldHint: false\n\t\t},\n\t\thandler: ({ text, at }) => {\n\t\t\tconst ed = editor();\n\t\t\tconst size = ed.state.doc.content.size;\n\t\t\tif (at !== undefined && at > size) {\n\t\t\t\tthrow new Error(`Position ${at} is outside the document (size ${size}).`);\n\t\t\t}\n\t\t\tconst { from, to } = ed.state.selection;\n\t\t\treplaceWithText(ed, at ?? from, at ?? to, text);\n\t\t\treturn { inserted: text.length, selection: selectionInfo(ed) };\n\t\t}\n\t};\n\n\tconst replaceSelection: AgentTool = {\n\t\tname: 'replace_selection',\n\t\ttitle: 'Replace selection',\n\t\tdescription: 'Replaces the selected content with literal text (an empty string deletes the selection).',\n\t\tinputSchema: {\n\t\t\ttype: 'object',\n\t\t\tproperties: { text: { type: 'string', description: 'Replacement text.' } },\n\t\t\trequired: ['text'],\n\t\t\tadditionalProperties: false\n\t\t},\n\t\tannotations: {\n\t\t\ttitle: 'Replace selection',\n\t\t\treadOnlyHint: false,\n\t\t\tdestructiveHint: true,\n\t\t\tidempotentHint: false,\n\t\t\topenWorldHint: false\n\t\t},\n\t\thandler: ({ text }) => {\n\t\t\tconst ed = editor();\n\t\t\tconst { from, to } = ed.state.selection;\n\t\t\treplaceWithText(ed, from, to, text);\n\t\t\treturn { replaced: { from, to }, selection: selectionInfo(ed) };\n\t\t}\n\t};\n\n\treturn {\n\t\tid: options.id,\n\t\tnamespace: options.namespace,\n\t\ttools: () => {\n\t\t\tconst ed = editor();\n\t\t\tif (!ed || ed.isDestroyed) return [];\n\t\t\treturn ed.isEditable ? [getText, insertText, replaceSelection] : [getText];\n\t\t},\n\t\tsubscribe: (notify) => {\n\t\t\tconst ed = editor();\n\t\t\t// `setEditable` emits `update`; the registry ignores signals that don't change the list.\n\t\t\ted.on('update', notify);\n\t\t\ted.on('destroy', notify);\n\t\t\treturn () => {\n\t\t\t\ted.off('update', notify);\n\t\t\t\ted.off('destroy', notify);\n\t\t\t};\n\t\t}\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Spreadsheet provider\n// ---------------------------------------------------------------------------\n\nconst CELL_REF_REGEX = /^\\$?([A-Za-z]{1,3})\\$?(\\d{1,7})$/;\n\nfunction parseCell(ref: string): { col: number; row: number } {\n\tconst match = CELL_REF_REGEX.exec(ref.trim());\n\tif (!match) throw new Error(`Invalid cell reference \"${ref}\". Use A1 notation, e.g. \"B3\".`);\n\treturn { col: colNameToIndex(match[1].toUpperCase()), row: parseInt(match[2], 10) - 1 };\n}\n\n/**\n * Tools for a spreadsheet controller: `get_range` (A1 notation, values and raw\n * input) and `set_cell` (raw input; values starting with `=` are formulas).\n */\nexport function createSpreadsheetToolProvider(\n\tcontroller: SpreadsheetController,\n\toptions: ToolProviderOptions\n): AgentToolProvider {\n\tconst lookup = (row: number, col: number) => {\n\t\tconst rowDef = controller.document.rows[row];\n\t\tconst colDef = controller.document.columns[col];\n\t\treturn rowDef && colDef ? { rowDef, colDef } : null;\n\t};\n\n\tconst getRange: AgentTool = {\n\t\tname: 'get_range',\n\t\ttitle: 'Read cells',\n\t\tdescription:\n\t\t\t'Reads a cell range in A1 notation (e.g. \"A1:C5\" or \"B2\") and returns computed values, raw input and errors.',\n\t\tinputSchema: {\n\t\t\ttype: 'object',\n\t\t\tproperties: {\n\t\t\t\trange: { type: 'string', description: 'A1-notation cell or range.' }\n\t\t\t},\n\t\t\trequired: ['range'],\n\t\t\tadditionalProperties: false\n\t\t},\n\t\tannotations: {\n\t\t\ttitle: 'Read cells',\n\t\t\treadOnlyHint: true,\n\t\t\tdestructiveHint: false,\n\t\t\tidempotentHint: true,\n\t\t\topenWorldHint: false\n\t\t},\n\t\thandler: ({ range }) => {\n\t\t\tconst [startRef, endRef = startRef] = String(range).split(':');\n\t\t\tconst start = parseCell(startRef);\n\t\t\tconst end = parseCell(endRef);\n\t\t\tconst rowCount = controller.document.rows.length;\n\t\t\tconst colCount = controller.document.columns.length;\n\t\t\tconst r0 = Math.min(start.row, end.row);\n\t\t\tconst r1 = Math.min(Math.max(start.row, end.row), rowCount - 1);\n\t\t\tconst c0 = Math.min(start.col, end.col);\n\t\t\tconst c1 = Math.min(Math.max(start.col, end.col), colCount - 1);\n\t\t\tconst values: unknown[][] = [];\n\t\t\tconst raw: string[][] = [];\n\t\t\tconst errors: Array<{ cell: string; error: string }> = [];\n\t\t\tfor (let r = r0; r <= r1; r++) {\n\t\t\t\tconst valueRow: unknown[] = [];\n\t\t\t\tconst rawRow: string[] = [];\n\t\t\t\tfor (let c = c0; c <= c1; c++) {\n\t\t\t\t\tconst found = lookup(r, c)!;\n\t\t\t\t\tconst cell = controller.getCell(found.rowDef.id, found.colDef.id);\n\t\t\t\t\tvalueRow.push(cell.error ?? cell.value);\n\t\t\t\t\trawRow.push(cell.raw);\n\t\t\t\t\tconst error = cell.error ?? (isFormulaError(cell.value) ? cell.value : undefined);\n\t\t\t\t\tif (error) errors.push({ cell: `${indexToColName(c)}${r + 1}`, error });\n\t\t\t\t}\n\t\t\t\tvalues.push(valueRow);\n\t\t\t\traw.push(rawRow);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\trange: values.length && values[0].length ? `${indexToColName(c0)}${r0 + 1}:${indexToColName(c1)}${r1 + 1}` : null,\n\t\t\t\tvalues,\n\t\t\t\traw,\n\t\t\t\terrors,\n\t\t\t\tsheet: { rows: rowCount, columns: colCount }\n\t\t\t};\n\t\t}\n\t};\n\n\tconst setCell: AgentTool = {\n\t\tname: 'set_cell',\n\t\ttitle: 'Set a cell',\n\t\tdescription: 'Sets the raw input of one cell (A1 notation). Values starting with \"=\" are formulas.',\n\t\tinputSchema: {\n\t\t\ttype: 'object',\n\t\t\tproperties: {\n\t\t\t\tcell: { type: 'string', description: 'A1-notation cell, e.g. \"B3\".' },\n\t\t\t\tvalue: { type: ['string', 'number', 'boolean'], description: 'Raw cell input.' }\n\t\t\t},\n\t\t\trequired: ['cell', 'value'],\n\t\t\tadditionalProperties: false\n\t\t},\n\t\tannotations: {\n\t\t\ttitle: 'Set a cell',\n\t\t\treadOnlyHint: false,\n\t\t\tdestructiveHint: true,\n\t\t\tidempotentHint: true,\n\t\t\topenWorldHint: false\n\t\t},\n\t\thandler: ({ cell, value }) => {\n\t\t\tconst { row, col } = parseCell(cell);\n\t\t\tconst found = lookup(row, col);\n\t\t\tif (!found) throw new Error(`Cell ${cell} is outside the sheet.`);\n\t\t\tconst { rowDef, colDef } = found;\n\t\t\tif (colDef.readOnly || controller.getCell(rowDef.id, colDef.id).readOnly || colDef.type === 'formula') {\n\t\t\t\tthrow new Error(`Cell ${cell} is read-only.`);\n\t\t\t}\n\t\t\tcontroller.setCellValue(rowDef.id, colDef.id, String(value));\n\t\t\tconst updated = controller.getCell(rowDef.id, colDef.id);\n\t\t\treturn { cell: cell.toUpperCase(), raw: updated.raw, value: updated.error ?? updated.value };\n\t\t}\n\t};\n\n\treturn { id: options.id, namespace: options.namespace, tools: [getRange, setCell] };\n}\n\n// ---------------------------------------------------------------------------\n// Static editor tools (tools.ts) as a provider\n// ---------------------------------------------------------------------------\n\nexport interface EditorMcpToolProviderOptions<\n\tTNode extends CanvasNode = CanvasNode,\n\tTEdge extends CanvasEdge = CanvasEdge\n> extends ToolProviderOptions {\n\t/** Current editor / canvas state. */\n\tgetSnapshot: () => EditorSnapshot<TNode, TEdge>;\n\t/** Applies planned steps (e.g. via your canvas runes / editor controller). */\n\tapplySteps: (steps: AgentStep<TNode, TEdge>[]) => void | Promise<void>;\n\t/** Optional `view_editor` implementation; the tool is only exposed when provided. */\n\tcaptureView?: (args: { scope?: 'viewport' | 'canvas'; format?: 'jpeg' | 'png'; maxEdge?: number }) =>\n\t\t| Promise<{ data: string; mimeType: string }>\n\t\t| { data: string; mimeType: string };\n\t/** Options forwarded to the planner. */\n\tplannerOptions?: Parameters<typeof planToolCall>[3];\n}\n\n/**\n * Exposes the static MCP tools from `tools.ts` (`get_editor_state`,\n * `edit_canvas`, `edit_prompt`, `view_editor`) through a `ToolRegistry`,\n * planning edits with `planToolCall` and handing the steps to `applySteps`.\n */\nexport function createEditorMcpToolProvider<\n\tTNode extends CanvasNode = CanvasNode,\n\tTEdge extends CanvasEdge = CanvasEdge\n>(options: EditorMcpToolProviderOptions<TNode, TEdge>): AgentToolProvider {\n\tconst edit = (name: string) => async (args: Record<string, any>) => {\n\t\tconst steps = planToolCall<TNode, TEdge>(name, args, options.getSnapshot(), options.plannerOptions);\n\t\tif (steps.length === 0) {\n\t\t\tthrow new Error(`Operation \"${args.operation}\" is missing required arguments.`);\n\t\t}\n\t\tawait options.applySteps(steps);\n\t\treturn { applied: steps.length, steps: steps.map((s) => s.type) };\n\t};\n\n\tconst tools: AgentTool[] = [\n\t\t{\n\t\t\t...MCP_TOOL_GET_EDITOR_STATE,\n\t\t\thandler: ({ includeDocument = true, includeCanvas = true }) => {\n\t\t\t\tconst snapshot = options.getSnapshot();\n\t\t\t\treturn {\n\t\t\t\t\t...(includeDocument && snapshot.document ? { document: snapshot.document } : {}),\n\t\t\t\t\t...(includeCanvas && snapshot.canvas ? { canvas: snapshot.canvas } : {})\n\t\t\t\t};\n\t\t\t}\n\t\t},\n\t\t{ ...MCP_TOOL_EDIT_CANVAS, handler: edit(MCP_TOOL_EDIT_CANVAS.name) },\n\t\t{ ...MCP_TOOL_EDIT_PROMPT, handler: edit(MCP_TOOL_EDIT_PROMPT.name) }\n\t] as AgentTool[];\n\n\tif (options.captureView) {\n\t\tconst capture = options.captureView;\n\t\ttools.push({\n\t\t\t...MCP_TOOL_VIEW_EDITOR,\n\t\t\thandler: async (args) => {\n\t\t\t\tconst image = await capture(args);\n\t\t\t\treturn { content: [{ type: 'image', data: image.data, mimeType: image.mimeType }] };\n\t\t\t}\n\t\t} as AgentTool);\n\t}\n\n\treturn { id: options.id, namespace: options.namespace, tools };\n}\n"]}
|
package/dist/agent/index.d.cts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { C as CanvasNode, b as CanvasEdge, X as XYPosition, S as Side } from '../types-DX4Mh4SL.cjs';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
export { J as JsonSchema, a as JsonSchemaType, S as SchemaValidationError, v as validateJsonSchema } from '../schema-DYDGdYL9.cjs';
|
|
3
|
+
import { A as AgentToolProvider } from '../registry-DnzpgY27.cjs';
|
|
4
|
+
export { a as AgentTool, b as AgentToolHandler, M as McpListToolsResult, c as McpToolContent, d as McpToolDefinition, e as McpToolResult, T as TOOL_NAMESPACE_SEPARATOR, f as ToolCallContext, g as ToolCallError, h as ToolErrorCode, i as ToolRegistry, j as ToolRegistryError, k as ToolRegistryOptions, l as createToolRegistry, t as toToolResult } from '../registry-DnzpgY27.cjs';
|
|
4
5
|
import { Editor } from '@tiptap/core';
|
|
5
|
-
import { S as SpreadsheetController } from '../controller-
|
|
6
|
+
import { S as SpreadsheetController } from '../controller-BhLLAoi8.cjs';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Agent presence state for visual multiplayer/agent choreography.
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { C as CanvasNode, b as CanvasEdge, X as XYPosition, S as Side } from '../types-DX4Mh4SL.js';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
export { J as JsonSchema, a as JsonSchemaType, S as SchemaValidationError, v as validateJsonSchema } from '../schema-DYDGdYL9.js';
|
|
3
|
+
import { A as AgentToolProvider } from '../registry-BDe-vlv3.js';
|
|
4
|
+
export { a as AgentTool, b as AgentToolHandler, M as McpListToolsResult, c as McpToolContent, d as McpToolDefinition, e as McpToolResult, T as TOOL_NAMESPACE_SEPARATOR, f as ToolCallContext, g as ToolCallError, h as ToolErrorCode, i as ToolRegistry, j as ToolRegistryError, k as ToolRegistryOptions, l as createToolRegistry, t as toToolResult } from '../registry-BDe-vlv3.js';
|
|
4
5
|
import { Editor } from '@tiptap/core';
|
|
5
|
-
import { S as SpreadsheetController } from '../controller-
|
|
6
|
+
import { S as SpreadsheetController } from '../controller-BhLLAoi8.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Agent presence state for visual multiplayer/agent choreography.
|