codecaine-cli 0.0.0-stage → 0.1.2

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.
@@ -0,0 +1,830 @@
1
+ /**
2
+ * What a bb-run agent can do to the design that is open on the board.
3
+ *
4
+ * These declarations live here, in the plugin package, rather than beside the
5
+ * code that answers them: this directory is registered with bb by path and its
6
+ * entry is loaded *inside the bb server process*, which has no way to import
7
+ * out of the editor's bundle. The renderer half — `src/editor/agent/tools.ts` —
8
+ * holds a handler per name and nothing else, and
9
+ * `src/editor/agent/__tests__/plugin-parity.test.ts` is what keeps the two sides
10
+ * from drifting: a tool declared here with no handler is one the model is told
11
+ * about and can never call, and neither side would say so at runtime.
12
+ *
13
+ * Deliberately dependency-free and side-effect-free, so that parity test can
14
+ * import it from Vitest, where none of bb's runtime exists.
15
+ */
16
+
17
+ /** Which layer a tool is about. An id is exact; a selector is the fallback for
18
+ * an agent that has only read the markup — see docs/canvas-api.md for the
19
+ * grammar it accepts. */
20
+ export const TARGET_SCHEMA = {
21
+ type: "object",
22
+ description:
23
+ "Layer handle: id (preferred — returned by listLayers/createLayer) or a CSS-shaped selector over the design.",
24
+ properties: {
25
+ id: { type: "string", description: "Layer id, as the document holds it." },
26
+ selector: {
27
+ type: "string",
28
+ description:
29
+ 'Selector fallback, matched against the design: ".Hero > button", "text", "#id".',
30
+ },
31
+ },
32
+ } as const;
33
+
34
+ /**
35
+ * The one description that promises something the editor's own
36
+ * `canvas.screenshot()` does not: a file on disk rather than a data URL.
37
+ *
38
+ * bb's provider bridge keeps only text content parts before the model sees a
39
+ * tool result (verified in bb-app 0.42.1,
40
+ * `packages/agent-runtime/src/shared/bridge-tool-calls.ts`), so an inline image
41
+ * reaches one provider and is dropped for every other. The path is therefore
42
+ * the contract, and the image part the plugin also returns is a bonus for the
43
+ * provider that keeps it.
44
+ */
45
+ const SCREENSHOT_DESCRIPTION =
46
+ "Picture of the design: the whole page, or one layer with `target`. Writes a PNG into the OS temp dir and returns { path, width, height } — read the file at `path` with your own file-reading tool to look at it. The shot is re-rendered from the design's own markup: no script runs and nothing animates, so a page whose script builds or moves things looks the same here working or broken — to see behaviour, setPageView mode \"interact\" then canvasPointer with shot: true, then mode \"design\" again. A hidden layer draws blank. Anything that would need a network request (a remote image, a webfont the machine lacks) draws as a fallback.";
47
+
48
+ /** Several layers at once, for the tools that act on a set. */
49
+ export const TARGETS_SCHEMA = {
50
+ type: "array",
51
+ description: "The layers, each { id } or { selector }.",
52
+ items: TARGET_SCHEMA,
53
+ } as const;
54
+
55
+ export interface CanvasToolDefinition {
56
+ name: string;
57
+ description: string;
58
+ inputSchema: Record<string, unknown>;
59
+ }
60
+
61
+ /** The tools, in the order bb advertises them. Adding one means adding its
62
+ * handler in `src/editor/agent/tools.ts` — the parity test fails until both
63
+ * sides exist. */
64
+ export const CANVAS_TOOLS = [
65
+ {
66
+ name: "listLayers",
67
+ description:
68
+ "List the layers of the open design, in document order: { count, layers: [{ id, name, type, tag, text, parentId }] }. With no argument it lists everything, which is how to get your bearings on a document you have not seen.",
69
+ inputSchema: {
70
+ type: "object",
71
+ properties: {
72
+ selector: {
73
+ type: "string",
74
+ description: "Only layers matching this selector.",
75
+ },
76
+ search: {
77
+ type: "string",
78
+ description:
79
+ "Free text over name, type, tag, text and id, best matches first. Ignored when `selector` is given.",
80
+ },
81
+ limit: {
82
+ type: "number",
83
+ description: "Cap the rows returned (default 200).",
84
+ },
85
+ },
86
+ },
87
+ },
88
+ {
89
+ name: "listAnnotations",
90
+ description:
91
+ "What the person has pinned on the running page in code mode: { count, annotations: [{ id, selector, label, body }] }. Each one is something they pointed at and said out loud, and `selector` is what finds that element in the project\u2019s own source. Empty when nothing is pinned.",
92
+ inputSchema: { type: "object", properties: {} },
93
+ },
94
+ {
95
+ name: "resolveAnnotation",
96
+ description:
97
+ "Take a pin off the running page, once what it asked for has actually been done. The pin is the person\u2019s record of what is still outstanding, so resolve one only when the change is written \u2014 not when you have described it. Answers { resolved: true }, or an error when no pin has that id.",
98
+ inputSchema: {
99
+ type: "object",
100
+ properties: {
101
+ id: {
102
+ type: "string",
103
+ description: "The annotation's id, as `listAnnotations` gives it and as the message that raised it names it.",
104
+ },
105
+ },
106
+ required: ["id"],
107
+ },
108
+ },
109
+ {
110
+ name: "getSelection",
111
+ description:
112
+ "What the person is pointing at: the layers selected on the board right now, as { count, layers: [{ id, name, type, tag, text }] }. Empty when nothing is selected. This is how to answer \"this\" and \"these\" — check it before asking which layer they mean.",
113
+ inputSchema: { type: "object", properties: {} },
114
+ },
115
+ {
116
+ name: "getLayer",
117
+ description:
118
+ "Everything about one layer: { id, name, type, tag, text, path, props, style, ownStyle, box, size, position, children, hidden, locked }. `style` is the look it resolves to; `ownStyle` is only what it declares itself. For an element inside a running project (`remote: true`) there is only `style`: its computed style with the browser's own defaults left out, so a property that is absent is at its default. A picture the editor holds is named `asset:media/images/<hash>.<ext>`, and `files` gives its path on this machine (desktop app) so you can open it — for a picture inside a running project too, which exists nowhere else: to use it in the project's code, copy that file into the project (e.g. `public/`) and point the element at the copy; pass the `asset:` name back anywhere — in markup, a src, a url() — and it is the same picture. Anything still inline comes back as a short `media:…` reference that works the same way.",
119
+ inputSchema: {
120
+ type: "object",
121
+ properties: { target: TARGET_SCHEMA },
122
+ required: ["target"],
123
+ },
124
+ },
125
+ {
126
+ name: "createLayer",
127
+ description:
128
+ "Put a new layer on the board. `type` is an element type the build knows (text, frame, image, …). Returns { id } of the new layer. Omit `parent` for a layer straight on the page, and omit `at` with it: the layer then lands in clear space beside what is already on the board, which is what you want unless the person named a place. Do not invent coordinates to avoid a collision — you cannot see the board and the editor can. `at` is ignored inside a parent that lays its own children out — an auto-layout frame decides where its children go.",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ type: { type: "string", description: "Element type from the registry." },
133
+ parent: {
134
+ type: "string",
135
+ description:
136
+ "Id of the layer to put it inside. If it is not found, list the layers again and use an id from that answer — never drop `parent` to get past the error, which puts the layer on the board instead of inside it. An id inside a running project changes when the page re-renders.",
137
+ },
138
+ index: {
139
+ type: "number",
140
+ description: "Where among the parent's children it goes.",
141
+ },
142
+ name: { type: "string" },
143
+ text: { type: "string", description: "Text content, for a text layer." },
144
+ style: {
145
+ type: "object",
146
+ description:
147
+ 'CSS declarations, camelCased or hyphenated: { "backgroundColor": "#111", "padding": 16 }.',
148
+ },
149
+ at: {
150
+ type: "object",
151
+ description:
152
+ "Board coordinates for a free-placed layer. Leave it out and the editor finds clear space beside the design — it can see the board and you cannot. Ignored inside a container that arranges its own children.",
153
+ properties: { x: { type: "number" }, y: { type: "number" } },
154
+ },
155
+ size: {
156
+ type: "object",
157
+ properties: {
158
+ width: { type: "number" },
159
+ height: { type: "number" },
160
+ },
161
+ },
162
+ },
163
+ required: ["type"],
164
+ },
165
+ },
166
+ {
167
+ name: "updateLayer",
168
+ description:
169
+ "Change one layer in a single undoable step: name, tag, text, style (merged), clearStyle (taken off), props, hidden, locked, script and packages. Two things to get right: a style write lands in whichever band and pseudo-state the editor is currently showing unless `at` says otherwise (pass at.state \"\" for the ordinary design), and taking a declaration off is clearStyle, not setting it to an empty string. Any other field is refused, not ignored: a layer's type never changes, position goes in style.left/top, a move is moveLayer. The answer reads back the style values it wrote.",
170
+ inputSchema: {
171
+ type: "object",
172
+ properties: {
173
+ target: TARGET_SCHEMA,
174
+ name: { type: "string" },
175
+ tag: { type: "string" },
176
+ text: { type: "string" },
177
+ style: { type: "object", description: "Declarations to merge in." },
178
+ clearStyle: {
179
+ type: "array",
180
+ items: { type: "string" },
181
+ description: "Declarations to take off outright.",
182
+ },
183
+ props: { type: "object" },
184
+ hidden: { type: "boolean" },
185
+ locked: { type: "boolean" },
186
+ script: {
187
+ type: "string",
188
+ description:
189
+ "The code the page runs for this layer in Interact mode and Preview — never on the design canvas, and only while the layer is inside a webpage. A module with `element` already bound to this layer's rendered element: select inside it (element.querySelector; the markup's class names are there to select by), not across the document, so every copy of the layer works on its own. Change what it touches with inline styles or attributes — the design has no stylesheet for a class you toggle. Tell the person to try it in Interact mode or Preview. On a webpage it is the page's own code instead. An empty string removes it.",
190
+ },
191
+ packages: {
192
+ type: "object",
193
+ description:
194
+ 'npm packages the script imports by bare name, name → version ({ "gsap": "3" }), loaded from esm.sh. Replaces the layer\'s list.',
195
+ },
196
+ at: {
197
+ type: "object",
198
+ description:
199
+ "Which band and pseudo-state the style is for. Defaults to what the editor is showing.",
200
+ properties: {
201
+ breakpoint: { type: "string" },
202
+ state: { type: "string" },
203
+ },
204
+ },
205
+ },
206
+ required: ["target"],
207
+ },
208
+ },
209
+ {
210
+ name: "replaceLayer",
211
+ description:
212
+ "Rewrite a layer as HTML, in place. This is the tool for a real change — a card, a section, a whole hero — because the markup carries structure, text and design in one call instead of twenty. The layer keeps its id, its place, its size and its name, so carry on addressing it by the id you already have — but its type becomes whatever the markup is (an image replaced by a <section> is now a section), and the answer says so under `changed`. A webpage stays a webpage: the markup replaces what is ON the page, and its code and bands stay. A live project or a 3D canvas cannot be replaced. Everything it was made of is replaced, so read it again before you touch a child of it. A <style> block's class rules are dropped — style with Tailwind classes or inline style, and put motion in a <script> (element.animate). If the markup makes no element the layer is left as it was. Tailwind classes are resolved, fonts the markup names are registered, and pictures it points at are fetched. A <script> in the markup, and its on* handlers, are kept on the new layer and run only in Interact mode and Preview (see updateLayer's `script`); in it `element` is that layer — the script runs as a module, so document.currentScript is null. Write real elements — <button>, <a href>, <input> of any type (range, checkbox, date…), <select> with <option>s, <textarea>, <label>, <form>, <details> — never a <div> styled to look like one. hover:, focus:, focus-visible:, active: and disabled: classes become that layer's own state designs, so give every control them in the same markup. Classes are resolved into styles on the way in and no stylesheet comes with them: a script changes inline styles or attributes (hidden, open, aria-*), never toggles a class. A running page never fires a form's submit event: handle the submit button's click. On an element inside a running project it works too, and the element gets a new id, given in `changed`.",
213
+ inputSchema: {
214
+ type: "object",
215
+ properties: {
216
+ target: TARGET_SCHEMA,
217
+ html: {
218
+ type: "string",
219
+ description:
220
+ "The replacement, as one HTML element (its children come with it).",
221
+ },
222
+ keepStyle: {
223
+ type: "boolean",
224
+ description:
225
+ "Carry the old layer's styles onto the new one, under the markup's own. Off by default — a button's design is mostly not a heading's.",
226
+ },
227
+ },
228
+ required: ["target", "html"],
229
+ },
230
+ },
231
+ {
232
+ name: "insertHtml",
233
+ description:
234
+ "Put new HTML on the board — inside `parent`, or free on the page when there is none. The same importer as replaceLayer, and the same reason to prefer it: one call for a whole block. A <script> in it is kept on the layer it made, as that layer's `script`, where `element` is that layer (document.currentScript is null: it runs as a module). Write real elements — <button>, <a href>, <input> of any type (range, checkbox, date…), <select> with <option>s, <textarea>, <label>, <form>, <details> — never a <div> styled to look like one. hover:, focus:, focus-visible:, active: and disabled: classes become that layer's own state designs, so give every control them in the same markup. Classes are resolved into styles on the way in and no stylesheet comes with them: a script changes inline styles or attributes (hidden, open, aria-*), never toggles a class. A <style> block's class rules are dropped, so style inline or with Tailwind and animate from the <script> (element.animate). A running page never fires a form's submit event: handle the submit button's click. To fill a webpage, pass the webpage as `parent` — never replace or delete the webpage itself. Free on the page and with no `at`, it lands in clear space beside what is already there rather than over it. Returns { ids } of what it made.",
235
+ inputSchema: {
236
+ type: "object",
237
+ properties: {
238
+ html: { type: "string", description: "HTML for what to add." },
239
+ parent: {
240
+ type: "string",
241
+ description:
242
+ "Id of the layer to put it inside. Omit for the page. If it is not found, list the layers again and use an id from that answer — never drop `parent` to get past the error, which puts the layer on the board instead of inside it. An id inside a running project changes when the page re-renders.",
243
+ },
244
+ at: {
245
+ type: "object",
246
+ description:
247
+ "Board coordinates. Leave it out and the editor finds clear space beside the design; only name a point when the person asked for one.",
248
+ properties: { x: { type: "number" }, y: { type: "number" } },
249
+ },
250
+ },
251
+ required: ["html"],
252
+ },
253
+ },
254
+ {
255
+ name: "holdLayer",
256
+ description:
257
+ "Take a layer while you rewrite it. The board shows it as yours — a band of colour round the frame, a chip beside its name — and everybody else's edits are refused until you release it; yours still land. Hold what you are REWRITING, so nothing is edited underneath a half-applied change. Do not hold what you are only reading, and do not hold the original when you are making variants BESIDE it — that layer stays the person's to work on. Always release what you hold, with releaseLayer. Called again on a layer you already hold, it picks the work back up (after askAboutLayer, say). Returns { id, phase, label }.",
258
+ inputSchema: {
259
+ type: "object",
260
+ properties: {
261
+ target: TARGET_SCHEMA,
262
+ label: {
263
+ type: "string",
264
+ description:
265
+ 'What you are doing, in two or three words — "Rewriting hero". It is shown on the frame while you work, so a person who walks up knows what is happening to their design.',
266
+ },
267
+ },
268
+ required: ["target"],
269
+ },
270
+ },
271
+ {
272
+ name: "askAboutLayer",
273
+ description:
274
+ "Say you need an answer before you can finish a layer you are holding. The hold stays — half your change is applied, which is exactly when an edit underneath does damage — and the board says so, with a sound and a button that takes the person to the layer. Ask your question in the conversation as well; this is what makes it visible on the board. Call holdLayer again on the same layer to pick the work back up. Returns { id, phase, question }.",
275
+ inputSchema: {
276
+ type: "object",
277
+ properties: {
278
+ target: TARGET_SCHEMA,
279
+ question: {
280
+ type: "string",
281
+ description: "What you need to know, in one sentence.",
282
+ },
283
+ },
284
+ required: ["target", "question"],
285
+ },
286
+ },
287
+ {
288
+ name: "releaseLayer",
289
+ description:
290
+ "Give a layer back. The hold ends at once and `message` is what the board says about it. Release everything you hold before your turn ends, including when you are giving up or the work failed — a layer left held refuses the person's own edits. It is safe to call on a layer that is not held. Returns { id, released }.",
291
+ inputSchema: {
292
+ type: "object",
293
+ properties: {
294
+ target: TARGET_SCHEMA,
295
+ message: {
296
+ type: "string",
297
+ description:
298
+ "One line about what happened, shown to the person — \"Rewrote the hero in the new type scale\". Say so plainly here when it did not work.",
299
+ },
300
+ },
301
+ required: ["target"],
302
+ },
303
+ },
304
+ {
305
+ name: "removeLayer",
306
+ description:
307
+ "Delete a layer and everything inside it. Returns { removed } — the ids that actually went. A layer you did not make in this session is the person's: deleting it is refused unless they asked for that layer to go (then pass userAsked: true). Never delete a layer to replace it with a new one — the id everything points at is lost. If one of your edits broke a layer, canvasHistory undo brings it back as it was.",
308
+ inputSchema: {
309
+ type: "object",
310
+ properties: {
311
+ target: TARGET_SCHEMA,
312
+ userAsked: {
313
+ type: "boolean",
314
+ description: "True only when the person asked, in this conversation, for this layer to be deleted.",
315
+ },
316
+ },
317
+ required: ["target"],
318
+ },
319
+ },
320
+ {
321
+ name: "moveLayer",
322
+ description:
323
+ "Move a layer the way dragging it does: NEST it inside another layer (`parent`, at `index`), take it out onto the board (`parent: \"board\"`), or REORDER it among its siblings (`index`, or `order`). Works across every boundary — into a running project, out of one, within one — through the same placement engine a drag uses, so it never jumps. One undo step. Answers the layer where it now is; its id changes when it crosses into or out of a project, so use the one returned.",
324
+ inputSchema: {
325
+ type: "object",
326
+ properties: {
327
+ target: TARGET_SCHEMA,
328
+ parent: {
329
+ type: "string",
330
+ description: "The new parent's id, or \"board\" for the board itself.",
331
+ },
332
+ index: {
333
+ type: "number",
334
+ description: "Place among the parent's children, from 0. The end by default.",
335
+ },
336
+ order: {
337
+ type: "string",
338
+ enum: ["front", "back", "forward", "backward"],
339
+ description: "Without `parent`: move among its siblings — in a flow, reading order; on the board, what overlaps what.",
340
+ },
341
+ at: {
342
+ type: "object",
343
+ description: "Board coordinates, for a layer landing on the board.",
344
+ properties: { x: { type: "number" }, y: { type: "number" } },
345
+ },
346
+ },
347
+ required: ["target"],
348
+ },
349
+ },
350
+ {
351
+ name: "duplicateLayers",
352
+ description:
353
+ "Copy layers, each beside itself in the same parent, with everything inside. Answers { ids, layers } — the copies, to go on editing.",
354
+ inputSchema: {
355
+ type: "object",
356
+ properties: { targets: TARGETS_SCHEMA },
357
+ required: ["targets"],
358
+ },
359
+ },
360
+ {
361
+ name: "groupLayers",
362
+ description:
363
+ "Put a frame round layers, or take one away. `as: \"group\"` (default) wraps them in a plain frame that keeps each where it is; `as: \"auto-layout\"` wraps them in a frame that ARRANGES them — a row, a column or a grid, read off where they sit unless `direction` says — which is \"put these in a row\"; `as: \"ungroup\"` dissolves the named frames into their parents. The layers need one parent in common. A webpage or live project is never grouped or ungrouped. Ungrouping a frame you did not make deletes the person's frame, so it needs userAsked: true. Answers the new frame, or the freed layers and the `dissolved` frames.",
364
+ inputSchema: {
365
+ type: "object",
366
+ properties: {
367
+ targets: TARGETS_SCHEMA,
368
+ as: { type: "string", enum: ["group", "auto-layout", "ungroup"] },
369
+ direction: { type: "string", enum: ["row", "column", "grid"] },
370
+ userAsked: {
371
+ type: "boolean",
372
+ description: "Ungroup only: true when the person asked for these frames to be taken away.",
373
+ },
374
+ },
375
+ required: ["targets"],
376
+ },
377
+ },
378
+ {
379
+ name: "scaleLayers",
380
+ description:
381
+ "Scale layers with everything in them — font sizes, padding, gaps, radii, borders, shadows and the children's places — so the design keeps its proportions (the K tool). Not a resize: resizing changes the box and leaves the type the size it was.",
382
+ inputSchema: {
383
+ type: "object",
384
+ properties: {
385
+ targets: TARGETS_SCHEMA,
386
+ factor: { type: "number", description: "1.5 is half again as big; 0.5 halves it." },
387
+ },
388
+ required: ["targets", "factor"],
389
+ },
390
+ },
391
+ {
392
+ name: "setLayerFont",
393
+ description:
394
+ "Set layers in a typeface AND register it with the design, in one step. Writing `fontFamily` with updateLayer changes nothing on screen for a family the design does not carry yet; this fetches it (Google Fonts by name) and sets it.",
395
+ inputSchema: {
396
+ type: "object",
397
+ properties: {
398
+ targets: TARGETS_SCHEMA,
399
+ family: { type: "string", description: "As it is written: \"Playfair Display\", \"Inter\"." },
400
+ weight: { type: "number" },
401
+ style: { type: "string", description: "\"italic\" or \"normal\"." },
402
+ },
403
+ required: ["targets", "family"],
404
+ },
405
+ },
406
+ {
407
+ name: "selectLayers",
408
+ description:
409
+ "Select layers on the board, so the person sees which ones you mean — the way to point back at something. An empty `targets` clears the selection. A view change: nothing to undo.",
410
+ inputSchema: {
411
+ type: "object",
412
+ properties: { targets: TARGETS_SCHEMA },
413
+ required: ["targets"],
414
+ },
415
+ },
416
+ {
417
+ name: "setPageView",
418
+ description:
419
+ "How a page on the board is framed and run — the Design panel's View controls and the toolbar's mode. `view`: \"whole-page\" frames the document at its full height and never scrolls (so scroll-driven animations never fire); \"in-view\" is a real window (1440×1024 unless it has been resized) that scrolls — switch to it before watching or driving anything scroll-linked. `device`: \"desktop\" | \"tablet\" | \"mobile\" (the page's bands). `motion`: \"stop\" holds every animation still, \"reduced\", \"playing\" runs them as a visitor sees them (the default). `mode`: \"interact\" hands the board's pointer to the pages on it, \"design\" gives it back. `preview`: true opens the page in Preview, false closes it. `theme`: \"light\" | \"dark\" shows a live project's running page or a webpage in that theme — its `.dark` class / `data-theme` and its `prefers-color-scheme` rules switched from outside (and a webpage's variables drawn in their dark mode), nothing written into the project or the design — and \"system\" gives it back to the page; the way to check a change in both themes. `target` is a live project, a webpage, or any element inside one. Answers the page's view as it now is.",
420
+ inputSchema: {
421
+ type: "object",
422
+ properties: {
423
+ target: TARGET_SCHEMA,
424
+ view: { type: "string", enum: ["whole-page", "in-view"] },
425
+ device: { type: "string", description: "A band id: \"desktop\", \"tablet\", \"mobile\"." },
426
+ motion: { type: "string", enum: ["stop", "reduced", "playing"] },
427
+ mode: { type: "string", enum: ["design", "interact"] },
428
+ preview: { type: "boolean" },
429
+ theme: { type: "string", enum: ["light", "dark", "system"] },
430
+ },
431
+ required: ["target"],
432
+ },
433
+ },
434
+ {
435
+ name: "canvasPointer",
436
+ description:
437
+ "Use a page with a REAL mouse and keyboard — the desktop app's own input, not script events — so :hover styles, drag libraries, wheel-driven smooth scrolling and focus behave exactly as for a visitor. `target` is the page (a live project or webpage) or a layer; the editor switches to Interact, brings every point on screen, plays `actions` in order and puts the mode back (`stay: true` keeps Interact — then end with setPageView mode \"design\", or the person is left in Interact). Each action's place is `on`/`from`/`to`: { selector } (the PAGE's selector inside a live project), { id }, or { x, y } in the target's own pixels from its top-left. Actions: { do: \"hover\" | \"move\" | \"click\" | \"double-click\" | \"right-click\" | \"down\" | \"up\", on }, { do: \"drag\", from, to, steps }, { do: \"wheel\", on, dy, dx } (dy > 0 scrolls down, sent as notches like a real wheel), { do: \"type\", text } (into whatever has focus — click a field first), { do: \"key\", press: \"Enter\", modifiers: [\"Shift\"] }, { do: \"wait\", ms }. `shot: true` answers a PNG path of the target as composited afterwards — hover state and all, which canvasScreenshot cannot show. Pair with observePage to read what the input changed.",
438
+ inputSchema: {
439
+ type: "object",
440
+ properties: {
441
+ target: TARGET_SCHEMA,
442
+ actions: {
443
+ type: "array",
444
+ items: {
445
+ type: "object",
446
+ properties: {
447
+ do: {
448
+ type: "string",
449
+ enum: ["hover", "move", "click", "double-click", "right-click", "down", "up", "drag", "wheel", "type", "key", "wait"],
450
+ },
451
+ on: { type: "object", description: "{ selector } | { id } | { x, y }" },
452
+ from: { type: "object" },
453
+ to: { type: "object" },
454
+ steps: { type: "number" },
455
+ dy: { type: "number" },
456
+ dx: { type: "number" },
457
+ text: { type: "string" },
458
+ press: { type: "string" },
459
+ modifiers: { type: "array", items: { type: "string" } },
460
+ ms: { type: "number" },
461
+ button: { type: "string", enum: ["left", "right", "middle"] },
462
+ },
463
+ required: ["do"],
464
+ },
465
+ },
466
+ shot: { type: "boolean" },
467
+ settleMs: { type: "number", description: "Pause before the picture, for a transition to finish. 250 by default." },
468
+ stay: { type: "boolean" },
469
+ },
470
+ required: ["target", "actions"],
471
+ },
472
+ },
473
+ {
474
+ name: "canvasHistory",
475
+ description:
476
+ "Undo or redo on the design — the same history the person's ⌘Z walks, shared with them: an undo takes back the last edit, whoever made it. This is how to repair an edit of yours that broke a layer: it comes back with its own id, which a rebuilt copy never has. Answers how many steps were taken and what they changed: `appeared`, `disappeared`, `changedLayers` — read them to check you did not take back the person's work.",
477
+ inputSchema: {
478
+ type: "object",
479
+ properties: {
480
+ action: { type: "string", enum: ["undo", "redo"] },
481
+ steps: { type: "number", description: "How many. 1 by default." },
482
+ },
483
+ required: ["action"],
484
+ },
485
+ },
486
+ {
487
+ name: "listElementTypes",
488
+ description:
489
+ "The element types this build can make — the vocabulary for createLayer's `type` — as { types: [{ type, label, description }] }. Most are ordinary markup; the canvas element is the one that runs code, and its entry carries the whole contract its scene code is written to.",
490
+ inputSchema: { type: "object", properties: {} },
491
+ },
492
+ {
493
+ name: "getCanvasScene",
494
+ description:
495
+ "A canvas element's scene: { id, name, preset, code, controls, bindings, values, params, actions, status, contract }. `controls` is the DialKit config the person tunes it with; `params` is what the code sees right now; `status` is what the board's runner made of the code — { state: \"running\" | \"starting\" | \"error\", error, frames } — so an error here is a bug in the code to fix.",
496
+ inputSchema: {
497
+ type: "object",
498
+ properties: { target: TARGET_SCHEMA },
499
+ required: ["target"],
500
+ },
501
+ },
502
+ {
503
+ name: "setCanvasScene",
504
+ description:
505
+ "Make or rewrite a canvas element — a <canvas> tag running a three.js scene, tuned with DialKit controls. The tool for anything DRAWN or ANIMATED rather than laid out: a 3D scene, a simulator, a game, a particle field, a data visualisation. Without `target` it creates a new canvas (in clear space, or inside `parent`); with one it rewrites that canvas in one undoable step. " +
506
+ "`code` is the BODY of an async function with THREE, renderer, scene, camera (at z=5), params, frame((time, delta) => …), resize, change((params, keys) => …), action(name, fn), pointer ({ x, y } in -1…1, down, inside), onPointer, keys (a Set of KeyboardEvent.code) and onKey in scope; `scene` is rendered with `camera` after every frame unless your code rendered itself. No DOM and no network: build geometry in code, draw text on an OffscreenCanvas and use it as a CanvasTexture. " +
507
+ "`controls` is a DialKit config — speed: [1, 0, 5, 0.1] (slider), color: \"#a78bfa\", wireframe: false, mode: { type: \"select\", options: [...] }, restart: { type: \"action\" }, and a plain object is a folder — read in the code as params.speed, params.folder.key. Answers { id, status, params }: status.error is your code failing — fix it and call again. getCanvasScene or listElementTypes gives the full contract.",
508
+ inputSchema: {
509
+ type: "object",
510
+ properties: {
511
+ target: TARGET_SCHEMA,
512
+ code: { type: "string", description: "The scene's code: the body of an async function." },
513
+ controls: {
514
+ type: "object",
515
+ description: "The DialKit config the person tunes the scene with.",
516
+ },
517
+ bindings: {
518
+ type: "object",
519
+ description:
520
+ "Which dials also drive the element's own style, so a dial can set the box itself: { radius: \"borderRadius\", fill: \"backgroundColor\" } (a folder's dial is \"folder.key\"; { prop, unit } picks the unit of a length). Bindable: borderRadius, borderWidth, gap, padding, fontSize, letterSpacing (a slider, in px unless a unit is given), opacity, fontWeight, lineHeight (a slider, unitless), backgroundColor, color, borderColor (a colour dial). Replaces what was bound.",
521
+ },
522
+ values: {
523
+ type: "object",
524
+ description: "Dial settings, merged over what is set.",
525
+ },
526
+ preset: {
527
+ type: "string",
528
+ description: "Start from a built-in scene instead: knot, galaxy, waves or dodge (a small game).",
529
+ },
530
+ name: { type: "string", description: "The layer's name, for a new canvas or a rename." },
531
+ parent: {
532
+ type: "string",
533
+ description:
534
+ "Id of the layer to put a new canvas inside. Omit for the board. If it is not found, list the layers again and use an id from that answer — never drop `parent` to get past the error, which puts the canvas on the board instead of inside it.",
535
+ },
536
+ size: {
537
+ type: "object",
538
+ description: "A new canvas's size in px. Defaults to 640 × 400.",
539
+ properties: { width: { type: "number" }, height: { type: "number" } },
540
+ },
541
+ },
542
+ },
543
+ },
544
+ {
545
+ name: "exportHtml",
546
+ description:
547
+ "The design as markup — the whole page, or one layer with `target`. This is the fastest way to read a document you are about to change. `responsive` and `states` emit the media queries and the :hover rules the design carries. Pictures are named `asset:media/images/<hash>.<ext>` (getLayer's `files` gives the path on disk); pass the name back in markup and it is the same picture. Anything still inline comes back as a short `media:…` reference that works the same way. A `target` inside a running project answers that element as its page has it — its own classes and attributes, its computed look inline — which is what to copy when moving a block from one project to another.",
548
+ inputSchema: {
549
+ type: "object",
550
+ properties: {
551
+ target: TARGET_SCHEMA,
552
+ mode: { type: "string", enum: ["fragment", "document"] },
553
+ styles: { type: "string", enum: ["inline", "classes"] },
554
+ responsive: { type: "boolean" },
555
+ states: { type: "boolean" },
556
+ },
557
+ },
558
+ },
559
+ {
560
+ name: "listVariables",
561
+ description:
562
+ "The CSS variables this board can reach: { collections: [{ id, name, modes, remote, source }], variables: [{ name, property, collection, category, values, resolved, alias, usedBy, owner }] }. Two kinds sit in one list and the difference matters. The DOCUMENT's own are this design's to create, rename and delete. A framed project's are read out of its running stylesheets \u2014 marked `remote`, with `source` naming the file they are declared in (`src/index.css` where the dev server could be traced) \u2014 and are that application's, not the design's. `values` is keyed by mode name (\"Light\", \"Dark\"), so a token with two themes reports both; `alias` names the variable a row points at instead of holding a value of its own, which is usually the answer to \"why is this the wrong colour\". Read this before changing a colour by hand: a design with a `--primary` wants that changed once, not a hex written onto forty layers.",
563
+ inputSchema: {
564
+ type: "object",
565
+ properties: {
566
+ scope: {
567
+ type: "string",
568
+ enum: ["all", "document", "project"],
569
+ description:
570
+ "Which half to list. Default `all`. `project` is the framed application's own scale, and is empty when no project is on the board.",
571
+ },
572
+ search: {
573
+ type: "string",
574
+ description:
575
+ "Free text over the name, the property and the value. A compiled Tailwind theme is several hundred rows; narrow it.",
576
+ },
577
+ limit: { type: "number", description: "Cap the rows returned (default 200)." },
578
+ },
579
+ },
580
+ },
581
+ {
582
+ name: "setVariable",
583
+ description:
584
+ "Create or change one CSS variable, by name. On the DOCUMENT this creates the variable when nothing holds that property and updates it when something does; every layer bound to it repaints, which is the point of using one. On a FRAMED PROJECT (a property `listVariables` reports as `remote`) the write goes into the running page under the selector that declares that theme, is counted as a change to that page, and is written into the project's own file when the person saves the page into code \u2014 so changing an application's `--primary` here is a real edit to their design system, not a preview. `value` is any CSS the property takes; `var(--other)` makes it an alias. Answers { property, scope, mode, created }.",
585
+ inputSchema: {
586
+ type: "object",
587
+ properties: {
588
+ name: {
589
+ type: "string",
590
+ description:
591
+ "The variable, as `listVariables` names it: `surface/primary`, or the property itself (`--surface-primary`). Slashes are groups.",
592
+ },
593
+ value: {
594
+ type: "string",
595
+ description:
596
+ "What it should say \u2014 `#155efa`, `16px`, `var(--color-neutral-0)` to alias another variable. An empty string on a project's variable takes this editor's override off and lets the file answer again.",
597
+ },
598
+ mode: {
599
+ type: "string",
600
+ description:
601
+ "Which column, by name or selector: `Dark`, `.dark`. The collection's first mode when left out.",
602
+ },
603
+ description: {
604
+ type: "string",
605
+ description: "One sentence on how it should be used. Document variables only.",
606
+ },
607
+ },
608
+ required: ["name", "value"],
609
+ },
610
+ },
611
+ {
612
+ name: "deleteVariable",
613
+ description:
614
+ "Take one of the DESIGN's variables out. Every use of it is written back as the value it resolved to, so nothing on the board changes appearance \u2014 the layers simply stop sharing a decision. A framed project's variables cannot be deleted from here: they are declarations in that application's own file. Answers { deleted, inlined }.",
615
+ inputSchema: {
616
+ type: "object",
617
+ properties: {
618
+ name: {
619
+ type: "string",
620
+ description: "The variable, as `listVariables` names it, or its property.",
621
+ },
622
+ },
623
+ required: ["name"],
624
+ },
625
+ },
626
+ {
627
+ name: "canvasReadPage",
628
+ description:
629
+ "Read a RUNNING project framed on the board by its own CSS selectors \u2014 the fast way to find out what is actually on somebody's application. Answers { matched, count, rows: [{ nodeId, tag, id, text, href, src, alt, value, checked, rect, visible }] }, and `nodeId` is a layer every other tool here takes, so read the page and then act on what you found. The selector is run by the PAGE's own querySelectorAll, so `:has()`, `[data-state=open]` and `:nth-of-type` work here and do not work in listLayers, which matches the editor's own grammar over the handful of fields a snapshot carries. This is the only way to an attribute at all: a snapshot carries none, so a link's target, a field's current value and a component's data- state are unreachable without it. `text` is the element's rendered words, not the truncated label listLayers shows. Ask for `fields` (classes, attrs, selector, html, path) and `styles` only when you need them \u2014 a page's class lists are longer than its content. Empty when no live project is framed; this tool does not read the design itself, which listLayers and exportHtml already answer. With SEVERAL projects framed and none selected it refuses and names them rather than guessing \u2014 pass `target`, or check getSelection for the one the person is looking at.",
630
+ inputSchema: {
631
+ type: "object",
632
+ properties: {
633
+ selector: {
634
+ type: "string",
635
+ description:
636
+ "A CSS selector in the PAGE's grammar, e.g. \"a[href]\", \"[data-state=open]\", \"section:has(h2)\". Omitted, the answer is the element named by `target`, or the page's top-level elements.",
637
+ },
638
+ target: {
639
+ type: "object",
640
+ description:
641
+ "Read inside this element instead of the whole page: the id of a live project, or of an element inside one (`live:...`, as this tool and listLayers return it).",
642
+ properties: { id: { type: "string" } },
643
+ },
644
+ fields: {
645
+ type: "array",
646
+ description:
647
+ "Extra per-row fields, each of which costs more than the rest of the row: \"classes\", \"attrs\", \"selector\", \"html\", \"path\".",
648
+ items: { type: "string", enum: ["classes", "attrs", "selector", "html", "path"] },
649
+ },
650
+ attrs: {
651
+ type: "array",
652
+ description: "Report exactly these attributes rather than all of them.",
653
+ items: { type: "string" },
654
+ },
655
+ styles: {
656
+ type: "array",
657
+ description: "Computed properties to report per row, as CSS names (\"font-size\").",
658
+ items: { type: "string" },
659
+ },
660
+ limit: { type: "number", description: "Rows to answer with (default 100, ceiling 500). `matched` says how many there were." },
661
+ textLimit: { type: "number", description: "Where each row's text is cut (default 200)." },
662
+ },
663
+ },
664
+ },
665
+ {
666
+ name: "observePage",
667
+ description:
668
+ "Watch a page that is NOT this design, to see how an effect on it works — the call to make when somebody points at a site or a running app and says \"do that\". Frames any address that allows framing (or a live project already on the board — any other layer id is refused, since a webpage on the board is watched with setPageView interact + canvasPointer), drives it, and answers per watched element: which CSS properties moved and at which scroll offsets, its `scrollPath` (where it sat on screen at each scroll step, folded into segments: `pinned` for sticky/fixed, `flow` for moving with the content, `parallax` with the rate it moves per pixel scrolled — so \"from A to B over 100px\" is read, not guessed), the page's own transitions, and its @keyframes read exactly out of the browser's animation system (real duration, real easing, real keyframes — the parts an approximation always drops). A screenshot shows the end state and says nothing about what happens between the two ends of a scroll. Nothing is written to the observed page. START WITH NO `watch`: the answer then carries `candidates`, the elements on that page that actually declare an effect, and you name two or three of their selectors in a second call. Read `notes` first — it says when an empty answer is a page that does nothing and when it is a gesture that never landed.",
669
+ inputSchema: {
670
+ type: "object",
671
+ properties: {
672
+ url: {
673
+ type: "string",
674
+ description:
675
+ "The page: an address, or the id of a live project already on the board.",
676
+ },
677
+ watch: {
678
+ type: "array",
679
+ items: { type: "string" },
680
+ description:
681
+ 'What to watch, each either a CSS selector read inside that page ("h1", "[data-slot=card] button") or the id of a layer already inside it (a "live:…" id from listLayers or getSelection). Leave it out on the first call to get `candidates` instead.',
682
+ },
683
+ plan: {
684
+ type: "array",
685
+ description:
686
+ 'Drive the page yourself, step by step — use this when the canned passes do not reach the thing you are looking at (a menu that has to be opened before its item can be hovered, a section three clicks in, a range of scroll you want read closely). Each step is answered with where the page actually ended up and what the pointer actually landed on. Steps: {do:"scroll",to:<px|"top"|"bottom"|selector>}, {do:"scrollBy",y:<px>}, {do:"hover",on:<selector>}, {do:"click",on:<selector>}, {do:"state",on:<selector>,state:"hover"|"active"|"focus"|"focus-visible"|"disabled"|""}, {do:"key",press:"Escape"}, {do:"wait",ms:200}, {do:"settle"}. A reading is taken after every step. NOTE a dispatched hover does not fire CSS :hover — pair {do:"hover"} with {do:"state",state:"hover"} on the same selector to see both a JavaScript handler and the page\'s own :hover rules.',
687
+ items: {
688
+ type: "object",
689
+ properties: {
690
+ do: {
691
+ type: "string",
692
+ enum: [
693
+ "scroll",
694
+ "scrollBy",
695
+ "hover",
696
+ "click",
697
+ "state",
698
+ "key",
699
+ "wait",
700
+ "settle",
701
+ ],
702
+ },
703
+ to: {
704
+ description:
705
+ 'For scroll: a pixel offset, "top", "bottom", or a selector to scroll to.',
706
+ },
707
+ y: { type: "number", description: "For scrollBy: pixels down." },
708
+ x: { type: "number", description: "For scrollBy: pixels across." },
709
+ on: {
710
+ type: "string",
711
+ description: "For hover, click and state: a CSS selector in that page.",
712
+ },
713
+ state: {
714
+ type: "string",
715
+ description: 'For state: the pseudo-state to show, or "" to clear it.',
716
+ },
717
+ press: { type: "string", description: "For key: the key name." },
718
+ ms: { type: "number", description: "For wait and settle." },
719
+ },
720
+ required: ["do"],
721
+ },
722
+ },
723
+ do: {
724
+ type: "string",
725
+ enum: ["scroll", "hover", "click"],
726
+ description:
727
+ "A canned pass, when you do not need a plan of your own. `scroll` goes down the whole document; `hover` and `click` act on the first watched selector. Ignored when `plan` is given.",
728
+ },
729
+ steps: {
730
+ type: "number",
731
+ description: "Readings across a canned scroll. Default 12.",
732
+ },
733
+ properties: {
734
+ type: "array",
735
+ items: { type: "string" },
736
+ description:
737
+ "CSS properties to sample instead of the default set. Naming a few gives a small answer.",
738
+ },
739
+ shots: {
740
+ type: "boolean",
741
+ description:
742
+ "Also take a picture of each watched element at each step, written as PNGs into the OS temp dir — the answer then carries `frames: [{ path, step, label, … }]` and you read those files with your own file-reading tool. They are rebuilt from the element's markup rather than captured as pixels, so a font the machine lacks draws as a fallback. Worth it when a number needs checking against a picture; it is the expensive part of the call.",
743
+ },
744
+ },
745
+ required: ["url"],
746
+ },
747
+ },
748
+ {
749
+ name: "observeVideo",
750
+ description:
751
+ "Stills out of a video layer on the board, at moments you name. Writes a JPEG per frame into the OS temp dir and returns { frames: [{ path, at, asked, width, height }] } — read the files at `path` with your own file-reading tool to look at them. Ask for one time (`at`), several (`times`), or a sweep (`from`/`to`/`every`). Use it to answer a question about something in a reference clip: what the transition at ten seconds does, what the type looks like in the third shot.",
752
+ inputSchema: {
753
+ type: "object",
754
+ properties: {
755
+ target: TARGET_SCHEMA,
756
+ at: { type: "number", description: "One moment, in seconds." },
757
+ times: {
758
+ type: "array",
759
+ items: { type: "number" },
760
+ description: "Or several moments.",
761
+ },
762
+ from: { type: "number", description: "Or a sweep: from this second…" },
763
+ to: { type: "number", description: "…to this one." },
764
+ every: {
765
+ type: "number",
766
+ description: "…taking a frame every this many seconds.",
767
+ },
768
+ limit: {
769
+ type: "number",
770
+ description: "How many frames a sweep may produce. Default 24.",
771
+ },
772
+ maxWidth: {
773
+ type: "number",
774
+ description: "Longest edge of each still. Default 1280.",
775
+ },
776
+ },
777
+ required: ["target"],
778
+ },
779
+ },
780
+ {
781
+ name: "setAppBrief",
782
+ description:
783
+ "Hand back an app description the person asked you to improve, from the Apps directory's Create with AI page. The text replaces what is in their description box, where they can edit it or undo it before asking for the app to be built. Call it only when asked to improve an app description, and write no files for it. Returns { delivered } — false when that page is no longer open, in which case give the text in the conversation instead.",
784
+ inputSchema: {
785
+ type: "object",
786
+ properties: {
787
+ text: {
788
+ type: "string",
789
+ description:
790
+ "The improved description, in plain prose: what the app does, what it reads and changes on the board, and what it connects to.",
791
+ },
792
+ },
793
+ required: ["text"],
794
+ },
795
+ },
796
+ {
797
+ name: "canvasScreenshot",
798
+ description: SCREENSHOT_DESCRIPTION,
799
+ inputSchema: {
800
+ type: "object",
801
+ properties: {
802
+ target: TARGET_SCHEMA,
803
+ width: { type: "number" },
804
+ height: { type: "number" },
805
+ scale: { type: "number", description: "Device-pixel multiplier." },
806
+ background: {
807
+ type: "string",
808
+ description:
809
+ "Painted under the design. Transparent by default, which is what a layer lifted off the board looks like.",
810
+ },
811
+ },
812
+ },
813
+ },
814
+ ] as const satisfies readonly CanvasToolDefinition[];
815
+
816
+ export type CanvasToolName = (typeof CANVAS_TOOLS)[number]["name"];
817
+
818
+ /** The one tool whose result carries image bytes the plugin must spill to a
819
+ * file before the model can be told about it. */
820
+ export const SCREENSHOT_TOOL: CanvasToolName = "canvasScreenshot";
821
+
822
+ /** And the ones whose results carry *several*. Same reason and same spill —
823
+ * bb keeps only text parts before most providers see a tool result, so a
824
+ * picture the model is meant to look at has to become a path.
825
+ *
826
+ * Unlike the screenshot, a result here is not only its pictures: an observed
827
+ * scene carries the tracks and the animations, which are the answer, and the
828
+ * frames ride alongside. So the spill replaces the frames *in place* and
829
+ * leaves everything else standing. */
830
+ export const FRAMES_TOOLS: readonly CanvasToolName[] = ["observeVideo", "observePage", "canvasPointer"];