@dev.fast/whiteboard 0.5.1 → 0.5.2-preview.20261104.146
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/dist/{account-alias-Bw3PT3Qo.js → account-alias-BlYamXAG.js} +5 -5
- package/dist/{agent-cli-kHRrwO4n.js → agent-cli-D7fF5xmm.js} +9 -9
- package/dist/agent-cli-DLibAL4u.js +2 -0
- package/dist/{agent-client-CMacGbMp.js → agent-client-BCEbSPWU.js} +4 -4
- package/dist/{authoring-tools-CiLy46kh.js → authoring-tools-CkfxTVJm.js} +12 -8
- package/dist/{background-server-eZWXoVcI.js → background-server-BraP8pJ9.js} +4 -4
- package/dist/{background-server-BLNHbksB.js → background-server-CwToVN1g.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-Bxmiqutp.js → cli-BpadBaGU.js} +1 -1
- package/dist/{cli-runner-DQPuu7kJ.js → cli-runner-BebYF50a.js} +22 -22
- package/dist/cli.js +2 -2
- package/dist/{fuzzy-match-DZjeOaPY.js → fuzzy-match-CxcsLQGx.js} +3 -3
- package/dist/{headless-host-Dl2KZ9IT.js → headless-host-FAlbekFD.js} +561 -415
- package/dist/{headless-host-B4r0s1ks.js → headless-host-cLipIufH.js} +1 -1
- package/dist/{import-YAIU6rB0.js → import-CZ1958L4.js} +234 -62
- package/dist/{local-data-C58vzLZR.js → local-data-DdFkxFK_.js} +41 -16
- package/dist/{mcp-COYWiGVn.js → mcp-Claiy9xE.js} +9 -6
- package/dist/{package-paths-Cg6fnaGp.js → package-paths-Dk7MOXxN.js} +1 -1
- package/dist/{profile--BYIjPkc.js → profile-Bsgzi9PA.js} +2 -2
- package/dist/profile-BxdzBxHW.js +2 -0
- package/dist/{remote-attach-C1prLjmj.js → remote-attach-LGTpwpUv.js} +4 -4
- package/dist/{remote-extensions-Bw029iIS.js → remote-extensions-BvV29_gk.js} +1 -1
- package/dist/{remote-extensions-MUUgbLJn.js → remote-extensions-L95HNwIH.js} +2 -2
- package/dist/{remote-language-server-z3rdHcLL.js → remote-language-server-BAznkpeu.js} +4 -4
- package/dist/{remote-uninstall-B3Slwor1.js → remote-uninstall-CLP79Cuk.js} +5 -5
- package/dist/{request-origin-B3_XI2VA.js → request-origin-D6B-zrL8.js} +27 -5
- package/dist/{review-app-launcher-BZEbDKuK.js → review-app-launcher-tTe8bTwg.js} +2 -2
- package/dist/{review-home-paths-LdGS4SpR.js → review-home-paths-BmZX2qiz.js} +1 -1
- package/dist/{review-telemetry-x8u-9EZb.js → review-telemetry-DACDMfqq.js} +4 -4
- package/dist/server/desktop-host.js +26 -38
- package/dist/{server-discovery-WmuFl2S3.js → server-discovery-CV5qf1J8.js} +2 -2
- package/dist/sharing/index.d.ts +137 -14
- package/dist/sharing/index.js +1 -1
- package/dist/{src-B2b8cCoI.js → src-BQDlJXkG.js} +44 -5
- package/dist/{src-DLWol7GZ.js → src-CrrW7lcY.js} +16 -1
- package/dist/{stored-document-migration-iwIQ4l5G.js → stored-document-migration-D1wK4yID.js} +3 -3
- package/dist/{thread-state-B7L1mpN_.js → thread-state-WTqSborE.js} +1 -1
- package/dist/{tool-failure-RLR8m9hq.js → tool-failure-YOFGHJCZ.js} +319 -55
- package/instructions/animation.md +69 -5
- package/instructions/animations/before-after-structure/README.md +27 -0
- package/instructions/animations/before-after-structure/animation.html +89 -0
- package/instructions/animations/before-after-structure/block.json +10 -0
- package/instructions/animations/before-after-structure/frames.json +313 -0
- package/instructions/animations/cache-fill-evict/README.md +24 -0
- package/instructions/animations/cache-fill-evict/animation.html +81 -0
- package/instructions/animations/cache-fill-evict/block.json +13 -0
- package/instructions/animations/cache-fill-evict/frames.json +79 -0
- package/instructions/animations/cache-fill-evict/scenarios.json +17 -0
- package/instructions/animations/log-replay/README.md +25 -0
- package/instructions/animations/log-replay/animation.html +86 -0
- package/instructions/animations/log-replay/block.json +10 -0
- package/instructions/animations/log-replay/frames.json +143 -0
- package/instructions/animations/log-replay/scenarios.json +18 -0
- package/instructions/animations/queue-workers/README.md +23 -0
- package/instructions/animations/queue-workers/animation.html +107 -0
- package/instructions/animations/queue-workers/block.json +13 -0
- package/instructions/animations/queue-workers/frames.json +119 -0
- package/instructions/animations/queue-workers/scenarios.json +18 -0
- package/instructions/animations/race-two-lanes/README.md +22 -0
- package/instructions/animations/race-two-lanes/animation.html +97 -0
- package/instructions/animations/race-two-lanes/block.json +1 -0
- package/instructions/animations/race-two-lanes/frames.json +234 -0
- package/instructions/animations/race-two-lanes/scenarios.json +19 -0
- package/instructions/animations/request-across-layers/README.md +24 -0
- package/instructions/animations/request-across-layers/animation.html +85 -0
- package/instructions/animations/request-across-layers/block.json +17 -0
- package/instructions/animations/request-across-layers/frames.json +58 -0
- package/instructions/animations/request-across-layers/scenarios.json +12 -0
- package/instructions/animations/request-timing/README.md +22 -0
- package/instructions/animations/request-timing/animation.html +120 -0
- package/instructions/animations/request-timing/block.json +21 -0
- package/instructions/animations/request-timing/markers.json +32 -0
- package/instructions/animations/request-timing/scenarios.json +4 -0
- package/instructions/animations/retry-backoff/README.md +22 -0
- package/instructions/animations/retry-backoff/animation.html +122 -0
- package/instructions/animations/retry-backoff/block.json +10 -0
- package/instructions/animations/retry-backoff/frames.json +65 -0
- package/instructions/animations/retry-backoff/scenarios.json +12 -0
- package/instructions/animations/state-machine/README.md +24 -0
- package/instructions/animations/state-machine/animation.html +95 -0
- package/instructions/animations/state-machine/block.json +9 -0
- package/instructions/animations/state-machine/frames.json +79 -0
- package/instructions/animations/state-machine/scenarios.json +12 -0
- package/instructions/animations/video-platform-architecture/README.md +24 -0
- package/instructions/animations/video-platform-architecture/animation.html +76 -0
- package/instructions/animations/video-platform-architecture/block.json +17 -0
- package/instructions/animations/video-platform-architecture/frames.json +97 -0
- package/instructions/animations/video-platform-architecture/scenarios.json +28 -0
- package/instructions/whiteboard.d.ts +168 -14
- package/package.json +4 -4
- package/src/animation-stage-kit.ts +150 -10
- package/src/cli-runner.ts +4 -1
- package/src/fixtures/blocks/clip.json +19 -0
- package/src/review-api/README.md +1 -0
- package/src/review-api/agent-cli.ts +3 -2
- package/src/review-api/animation-drafts.ts +151 -47
- package/src/review-api/animation-references.ts +92 -0
- package/src/review-api/animation-render.ts +106 -0
- package/src/review-api/authoring-tools.ts +10 -3
- package/src/review-api/document-text.ts +27 -12
- package/src/review-api/http.ts +52 -6
- package/src/review-api/instructions.ts +1 -1
- package/src/review-api/mcp.ts +10 -5
- package/src/review-api/public-tools.ts +71 -2
- package/src/review-api/store.ts +89 -12
- package/src/server/desktop-server.ts +17 -5
- package/src/server/headless-host.ts +4 -0
- package/src/server/review-gateway.ts +1 -0
- package/src/server/review-server-core.ts +71 -12
- package/src/sharing/client.ts +129 -32
- package/src/sharing/export.ts +42 -5
- package/src/sharing/file.ts +8 -0
- package/src/sharing/host.ts +202 -73
- package/src/sharing/import.ts +125 -33
- package/src/ui-telemetry-events.ts +25 -1
- package/dist/agent-cli-B8KBHuh_.js +0 -2
- package/dist/profile-BjcXi-oH.js +0 -2
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { m as isStringValue, n as isJsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
|
|
2
|
-
import { M as checkReferences, j as blocks, ot as ReviewInputError } from "./src-
|
|
3
|
-
import { l as boardTitle } from "./src-
|
|
4
|
-
import "./review-home-paths-
|
|
5
|
-
import { n as callAuthoringTool, s as ReviewApiError, t as ToolText } from "./agent-client-
|
|
2
|
+
import { M as checkReferences, j as blocks, ot as ReviewInputError } from "./src-BQDlJXkG.js";
|
|
3
|
+
import { l as boardTitle } from "./src-CrrW7lcY.js";
|
|
4
|
+
import "./review-home-paths-BmZX2qiz.js";
|
|
5
|
+
import { n as callAuthoringTool, s as ReviewApiError, t as ToolText } from "./agent-client-BCEbSPWU.js";
|
|
6
6
|
import { N as devReviewHome } from "./s3-config-CjcLyFV5.js";
|
|
7
|
-
import { t as findReviewPackageRoot } from "./package-paths-
|
|
8
|
-
import { m as isMissingFileError } from "./thread-state-
|
|
7
|
+
import { t as findReviewPackageRoot } from "./package-paths-Dk7MOXxN.js";
|
|
8
|
+
import { m as isMissingFileError } from "./thread-state-WTqSborE.js";
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
10
|
import { z } from "zod";
|
|
11
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import { chmod, cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
//#region src/animation-stage-kit.ts
|
|
14
14
|
/** Every --wb-<name> the stage sets, by group, with what it is for. The
|
|
@@ -31,7 +31,9 @@ const STAGE_VARIABLES = [
|
|
|
31
31
|
"modified",
|
|
32
32
|
"added-bg",
|
|
33
33
|
"removed-bg",
|
|
34
|
-
"modified-bg"
|
|
34
|
+
"modified-bg",
|
|
35
|
+
"teal",
|
|
36
|
+
"violet"
|
|
35
37
|
]],
|
|
36
38
|
["Faces, by role: sans for labels and controls, mono for ids, paths and values, serif for prose, code for source text. Name no font yourself", [
|
|
37
39
|
"font-sans",
|
|
@@ -126,7 +128,7 @@ const CLASSES = [
|
|
|
126
128
|
[".wb-grid", "A grid of equal columns; set style=\"--wb-columns: 4\" (default 3)."],
|
|
127
129
|
[".wb-layers", "Children stacked in one place, for states that replace each other."],
|
|
128
130
|
[".wb-lane", "A position: relative track with a dashed center line, for a static token snapshot."],
|
|
129
|
-
[".wb-card", "A box with a title and note:
|
|
131
|
+
[".wb-card", "A box with a title and note: a .wb-card-head holding a .wb-card-dot and the .wb-card-title, then a .wb-card-note. The scene's cards are these."],
|
|
130
132
|
[".wb-node", "A centered box for a hand-made layout; a <small> inside is its second line."],
|
|
131
133
|
[".wb-well", "An inset area that groups parts, such as a process or a machine boundary."],
|
|
132
134
|
[".wb-rule", "A divider (<hr class=\"wb-rule\">)."],
|
|
@@ -138,7 +140,10 @@ const CLASSES = [
|
|
|
138
140
|
[".wb-prose", "A sentence of explanation, in the board's reading face."],
|
|
139
141
|
[".wb-mono, .wb-code", "Ids, paths and values in the mono face; .wb-code is inline code on a well."],
|
|
140
142
|
[".wb-muted, .wb-faint", "Text that recedes."],
|
|
141
|
-
[".wb-shape, .wb-shape-text", "An SVG shape drawn like a card, and the text inside it."]
|
|
143
|
+
[".wb-shape, .wb-shape-text", "An SVG shape drawn like a card, and the text inside it."],
|
|
144
|
+
[".wb-group", "A washed box behind a scene's cards with a .wb-group-title; scene.group draws one."],
|
|
145
|
+
[".wb-legend", "A row of .wb-legend-entry glyphs and labels under a scene; scene.legend draws one."],
|
|
146
|
+
["[data-wb-paint]", "When an agent writes the block, its parts paint in: data-wb-paint=\"3\" orders a part (numbers first, then document order), \"none\" shows it and its subtree at once; data-wb-paint-style=\"fade|draw|wipe|none\" picks the effect."]
|
|
142
147
|
];
|
|
143
148
|
/** The CSS every stage loads before its own markup. */
|
|
144
149
|
const STAGE_KIT_CSS = `:root { --wb-duration: 450ms; color-scheme: light dark; }
|
|
@@ -146,6 +151,7 @@ const STAGE_KIT_CSS = `:root { --wb-duration: 450ms; color-scheme: light dark; }
|
|
|
146
151
|
button { color: inherit; font: inherit; }
|
|
147
152
|
html, body { margin: 0; height: 100%; }
|
|
148
153
|
html[data-wb-auto-size] body { height: auto; display: flow-root; }
|
|
154
|
+
html[data-wb-unpainted] body { visibility: hidden; }
|
|
149
155
|
body {
|
|
150
156
|
overflow: hidden;
|
|
151
157
|
background: transparent;
|
|
@@ -200,9 +206,76 @@ body {
|
|
|
200
206
|
.wb-node { min-width: 88px; text-align: center; }
|
|
201
207
|
.wb-node small { display: block; color: var(--wb-ink-faint); font-size: var(--wb-text-micro); }
|
|
202
208
|
.wb-card-head { display: flex; align-items: center; gap: var(--wb-space-6); }
|
|
203
|
-
|
|
209
|
+
/* The dot says what state a card is in: it shows only with a tone or a
|
|
210
|
+
change mark, in that colour, so a plain card carries no unexplained mark. */
|
|
211
|
+
.wb-card-dot { display: none; flex: none; width: 7px; height: 7px; border-radius: var(--wb-radius-round); background: var(--wb-ink-faint); }
|
|
212
|
+
.wb-card:is([data-tone], [data-change]) .wb-card-dot { display: block; }
|
|
204
213
|
.wb-card-title { min-width: 0; overflow-wrap: anywhere; font-weight: var(--wb-weight-semibold); }
|
|
205
214
|
.wb-card-note { color: var(--wb-ink-muted); overflow-wrap: anywhere; font-size: var(--wb-text-micro); min-height: 12px; }
|
|
215
|
+
|
|
216
|
+
/* A store's text sits in the middle of its drum or cylinder, as a label on
|
|
217
|
+
a tank does; a pill's too. */
|
|
218
|
+
:where(.wb-card, .wb-node):is([data-form="cylinder"], [data-form="drum"], [data-form="pill"]) { align-items: center; text-align: center; }
|
|
219
|
+
.wb-card:is([data-form="cylinder"], [data-form="drum"], [data-form="pill"]) :is(.wb-card-head, .wb-items) { justify-content: center; }
|
|
220
|
+
/* Shapes: a card's or node's silhouette says what it stands for before
|
|
221
|
+
its title does. data-form is box (the default), pill, cut (a branch),
|
|
222
|
+
cylinder (a database), drum (a cache), stack (a queue), window (a
|
|
223
|
+
client) or bars (a service of this codebase). data-outline is solid,
|
|
224
|
+
dashed (outside the system) or dotted. data-color is the resting border
|
|
225
|
+
colour: ink, accent, teal or violet; a tone takes over while set. The
|
|
226
|
+
scene's cards take these as options. These rules and the change marks
|
|
227
|
+
weigh one class, as the card rules do: a hand-made layout's own rule for
|
|
228
|
+
its nodes, say position: absolute, comes later and wins. A tone or a
|
|
229
|
+
highlight still takes over. */
|
|
230
|
+
:where(.wb-card, .wb-node)[data-form] { position: relative; }
|
|
231
|
+
:where(.wb-card, .wb-node)[data-form]::before, :where(.wb-card, .wb-node)[data-form]::after { pointer-events: none; }
|
|
232
|
+
:where(.wb-card, .wb-node)[data-form="pill"] { padding-inline: var(--wb-space-16); border-radius: var(--wb-radius-pill); }
|
|
233
|
+
:where(.wb-card, .wb-node)[data-form="cut"] { border-radius: var(--wb-space-12); corner-shape: bevel; }
|
|
234
|
+
:where(.wb-card, .wb-node)[data-form="cylinder"] { padding-top: var(--wb-space-28); padding-bottom: var(--wb-space-12); border-radius: 50% / var(--wb-space-10); }
|
|
235
|
+
:where(.wb-card, .wb-node)[data-form="cylinder"]::before {
|
|
236
|
+
content: ""; position: absolute; inset: -1px -1px auto; height: var(--wb-space-20);
|
|
237
|
+
border: 1px solid; border-color: inherit; border-radius: 50%; background: inherit;
|
|
238
|
+
}
|
|
239
|
+
:where(.wb-card, .wb-node)[data-form="drum"] { padding-left: var(--wb-space-28); padding-right: var(--wb-space-16); border-radius: var(--wb-space-12) / 50%; }
|
|
240
|
+
:where(.wb-card, .wb-node)[data-form="drum"]::before {
|
|
241
|
+
content: ""; position: absolute; inset: -1px auto -1px -1px; width: var(--wb-space-24);
|
|
242
|
+
border: 1px solid; border-color: inherit; border-radius: 50%; background: inherit;
|
|
243
|
+
}
|
|
244
|
+
:where(.wb-card, .wb-node)[data-form="stack"] {
|
|
245
|
+
margin-right: var(--wb-space-8); margin-bottom: var(--wb-space-8);
|
|
246
|
+
box-shadow: 4px 4px 0 -1px var(--wb-surface), 4px 4px 0 0 var(--wb-rule), 8px 8px 0 -1px var(--wb-surface), 8px 8px 0 0 var(--wb-rule);
|
|
247
|
+
}
|
|
248
|
+
:where(.wb-card, .wb-node)[data-form="window"] { padding-top: var(--wb-space-16); }
|
|
249
|
+
:where(.wb-card, .wb-node)[data-form="window"]::before { content: ""; position: absolute; top: var(--wb-space-8); right: 0; left: 0; border-top: 1px solid; border-color: inherit; }
|
|
250
|
+
:where(.wb-card, .wb-node)[data-form="bars"] { padding-inline: var(--wb-space-18); }
|
|
251
|
+
:where(.wb-card, .wb-node)[data-form="bars"]::before, :where(.wb-card, .wb-node)[data-form="bars"]::after { content: ""; position: absolute; top: 0; bottom: 0; border-left: 1px solid; border-color: inherit; }
|
|
252
|
+
:where(.wb-card, .wb-node)[data-form="bars"]::before { left: var(--wb-space-6); }
|
|
253
|
+
:where(.wb-card, .wb-node)[data-form="bars"]::after { right: var(--wb-space-6); }
|
|
254
|
+
/* What the change under review did to a part: its dot and a stripe down its
|
|
255
|
+
left edge in the change colour, at rest; a removed part's title is struck.
|
|
256
|
+
A frame's tone still colours the whole card over it. */
|
|
257
|
+
:where(.wb-card, .wb-node)[data-change] { border-left-width: 3px; }
|
|
258
|
+
:where(.wb-card, .wb-node)[data-change="added"] { border-left-color: var(--wb-added); }
|
|
259
|
+
:where(.wb-card, .wb-node)[data-change="modified"] { border-left-color: var(--wb-modified); }
|
|
260
|
+
:where(.wb-card, .wb-node)[data-change="removed"] { border-left-color: var(--wb-removed); }
|
|
261
|
+
.wb-card[data-change="added"] .wb-card-dot { background: var(--wb-added); }
|
|
262
|
+
.wb-card[data-change="modified"] .wb-card-dot { background: var(--wb-modified); }
|
|
263
|
+
.wb-card[data-change="removed"] .wb-card-dot { background: var(--wb-removed); }
|
|
264
|
+
.wb-card[data-change="removed"] .wb-card-title { color: var(--wb-ink-muted); text-decoration: line-through; }
|
|
265
|
+
:where(.wb-card, .wb-node)[data-outline="dashed"] { border-style: dashed; }
|
|
266
|
+
:where(.wb-card, .wb-node)[data-outline="dotted"] { border-style: dotted; }
|
|
267
|
+
:where(.wb-card, .wb-node)[data-color="ink"] { border-color: var(--wb-ink); }
|
|
268
|
+
:where(.wb-card, .wb-node)[data-color="accent"] { border-color: var(--wb-accent); }
|
|
269
|
+
:where(.wb-card, .wb-node)[data-color="teal"] { border-color: var(--wb-teal); }
|
|
270
|
+
:where(.wb-card, .wb-node)[data-color="violet"] { border-color: var(--wb-violet); }
|
|
271
|
+
.wb-card[data-color="ink"] .wb-card-dot { background: var(--wb-ink); }
|
|
272
|
+
.wb-card[data-color="accent"] .wb-card-dot { background: var(--wb-accent); }
|
|
273
|
+
.wb-card[data-color="teal"] .wb-card-dot { background: var(--wb-teal); }
|
|
274
|
+
.wb-card[data-color="violet"] .wb-card-dot { background: var(--wb-violet); }
|
|
275
|
+
.wb-shape[data-color="ink"] { stroke: var(--wb-ink); }
|
|
276
|
+
.wb-shape[data-color="accent"] { stroke: var(--wb-accent); }
|
|
277
|
+
.wb-shape[data-color="teal"] { stroke: var(--wb-teal); }
|
|
278
|
+
.wb-shape[data-color="violet"] { stroke: var(--wb-violet); }
|
|
206
279
|
.wb-well { padding: var(--wb-space-12); border: 1px solid var(--wb-rule-soft); border-radius: var(--wb-radius-surface); background: var(--wb-well); }
|
|
207
280
|
.wb-well :is(.wb-item, .wb-chip, .wb-badge, .wb-code) { background: var(--wb-surface); }
|
|
208
281
|
.wb-rule { margin: var(--wb-space-12) 0; border: 0; border-top: 1px solid var(--wb-rule); }
|
|
@@ -266,10 +339,15 @@ body {
|
|
|
266
339
|
.wb-shape[data-tone="modified"] { stroke: var(--wb-modified); fill: var(--wb-modified-bg); }
|
|
267
340
|
.wb-shape[data-tone="muted"] { stroke-dasharray: 4 3; }
|
|
268
341
|
:is(.wb-card, .wb-node)[data-wb-highlit] { border-color: var(--wb-accent); box-shadow: 0 0 0 3px var(--wb-accent-soft); }
|
|
342
|
+
:is(.wb-card, .wb-node)[data-form="stack"][data-wb-highlit] {
|
|
343
|
+
box-shadow: 0 0 0 3px var(--wb-accent-soft), 4px 4px 0 -1px var(--wb-surface), 4px 4px 0 0 var(--wb-rule), 8px 8px 0 -1px var(--wb-surface), 8px 8px 0 0 var(--wb-rule);
|
|
344
|
+
}
|
|
269
345
|
.wb-shape[data-wb-highlit] { stroke: var(--wb-accent); stroke-width: 2; }
|
|
270
346
|
|
|
271
347
|
/* Connectors and tokens. */
|
|
272
|
-
|
|
348
|
+
/* Above the cards, which sit above their groups: an edge's head meets a
|
|
349
|
+
card's border, and a chip stays readable as it passes one. */
|
|
350
|
+
.wb-arrows, .wb-tokens { position: fixed; inset: 0; z-index: 2; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
|
|
273
351
|
.wb-line {
|
|
274
352
|
fill: none;
|
|
275
353
|
stroke: var(--wb-ink-faint);
|
|
@@ -288,6 +366,17 @@ body {
|
|
|
288
366
|
.wb-arrow[data-tone="removed"] .wb-line { stroke: var(--wb-removed); marker-end: url(#wb-head-removed); }
|
|
289
367
|
.wb-arrow[data-tone="modified"] .wb-line { stroke: var(--wb-modified); marker-end: url(#wb-head-modified); }
|
|
290
368
|
.wb-arrow[data-wb-headless] .wb-line { marker-end: none; }
|
|
369
|
+
/* An edge carrying one of the frame's messages takes the marker, label too. */
|
|
370
|
+
.wb-arrow[data-wb-sent] .wb-line { stroke: var(--wb-accent); marker-end: url(#wb-head-accent); }
|
|
371
|
+
.wb-arrow[data-wb-sent] .wb-label { fill: var(--wb-accent); }
|
|
372
|
+
.wb-arrow[data-wb-sent="added"] .wb-line { stroke: var(--wb-added); marker-end: url(#wb-head-added); }
|
|
373
|
+
.wb-arrow[data-wb-sent="added"] .wb-label { fill: var(--wb-added); }
|
|
374
|
+
.wb-arrow[data-wb-sent="removed"] .wb-line { stroke: var(--wb-removed); marker-end: url(#wb-head-removed); }
|
|
375
|
+
.wb-arrow[data-wb-sent="removed"] .wb-label { fill: var(--wb-removed); }
|
|
376
|
+
.wb-arrow[data-wb-sent="modified"] .wb-line { stroke: var(--wb-modified); marker-end: url(#wb-head-modified); }
|
|
377
|
+
.wb-arrow[data-wb-sent="modified"] .wb-label { fill: var(--wb-modified); }
|
|
378
|
+
.wb-arrow[data-wb-sent="muted"] .wb-line { stroke: var(--wb-ink-faint); marker-end: url(#wb-head); }
|
|
379
|
+
.wb-arrow[data-wb-sent="muted"] .wb-label { fill: var(--wb-ink-muted); }
|
|
291
380
|
.wb-label {
|
|
292
381
|
fill: var(--wb-ink-muted);
|
|
293
382
|
font: var(--wb-text-micro) var(--wb-font-mono);
|
|
@@ -322,16 +411,53 @@ body {
|
|
|
322
411
|
.wb-token[data-labelled][data-tone="muted"] { color: var(--wb-token-muted-ink); }
|
|
323
412
|
|
|
324
413
|
/* The scene. */
|
|
325
|
-
.wb-scene { display: grid; gap: var(--wb-space-28) var(--wb-space-48); padding: var(--wb-space-24); align-items:
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
.
|
|
414
|
+
.wb-scene { display: grid; gap: var(--wb-space-28) var(--wb-space-48); padding: var(--wb-space-24); align-items: center; justify-items: stretch; min-width: 0; }
|
|
415
|
+
/* A card is as wide as what it says, within bounds, and sits mid-column, so
|
|
416
|
+
a wide column does not stretch it into a slab. A table keeps its column;
|
|
417
|
+
a scene's own CSS may set any width. The scene sets its own min-width from
|
|
418
|
+
the columns its cards make, and the block scrolls it sideways when
|
|
419
|
+
narrower. */
|
|
420
|
+
.wb-scene > .wb-card:not([data-kind="table"]) { justify-self: center; width: fit-content; min-width: 140px; max-width: 300px; }
|
|
331
421
|
.wb-scene > [data-wb-state="before"], .wb-scene > [data-wb-state="after"],
|
|
332
422
|
.wb-items > [data-wb-state="before"], .wb-items > [data-wb-state="after"],
|
|
333
423
|
.wb-table tr[data-wb-state="before"], .wb-table tr[data-wb-state="after"] { display: none; }
|
|
334
424
|
.wb-scene-top { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--wb-space-8); margin-bottom: var(--wb-space-4); }
|
|
425
|
+
/* A group is a washed box behind the cards of its cells, bleeding into the
|
|
426
|
+
gaps around them, titled in its top-left. Cards paint over it. */
|
|
427
|
+
.wb-scene > .wb-card { position: relative; z-index: 1; }
|
|
428
|
+
/* Groups bleed into the gaps around their cells, so a grouped scene opens
|
|
429
|
+
its rows and columns to keep the boxes apart and give edges between
|
|
430
|
+
layers room to be read. The top bleed holds the title clear of the cards
|
|
431
|
+
below it, ring and all. */
|
|
432
|
+
.wb-scene:has(> .wb-group) { row-gap: var(--wb-space-72); column-gap: var(--wb-space-72); padding: var(--wb-space-32) var(--wb-space-24); }
|
|
433
|
+
.wb-group {
|
|
434
|
+
position: relative; z-index: 0; align-self: stretch; justify-self: stretch;
|
|
435
|
+
margin: calc(-1 * var(--wb-space-28)) calc(-1 * var(--wb-space-16)) calc(-1 * var(--wb-space-10));
|
|
436
|
+
border: 1px solid var(--wb-rule-soft); border-radius: var(--wb-radius-surface); background: var(--wb-well);
|
|
437
|
+
transition: opacity var(--wb-duration) ease;
|
|
438
|
+
}
|
|
439
|
+
.wb-group-title { position: absolute; top: var(--wb-space-4); left: var(--wb-space-12); color: var(--wb-ink-muted); font: var(--wb-text-micro)/var(--wb-leading-normal) var(--wb-font-mono); letter-spacing: var(--wb-tracking-chrome); text-transform: uppercase; white-space: nowrap; }
|
|
440
|
+
.wb-group[data-color="accent"] { border-color: color-mix(in srgb, var(--wb-accent) 30%, transparent); background: color-mix(in srgb, var(--wb-accent) 7%, transparent); }
|
|
441
|
+
.wb-group[data-color="accent"] .wb-group-title { color: var(--wb-accent); }
|
|
442
|
+
.wb-group[data-color="teal"] { border-color: color-mix(in srgb, var(--wb-teal) 36%, transparent); background: color-mix(in srgb, var(--wb-teal) 8%, transparent); }
|
|
443
|
+
.wb-group[data-color="teal"] .wb-group-title { color: var(--wb-teal); }
|
|
444
|
+
.wb-group[data-color="violet"] { border-color: color-mix(in srgb, var(--wb-violet) 36%, transparent); background: color-mix(in srgb, var(--wb-violet) 8%, transparent); }
|
|
445
|
+
.wb-group[data-color="violet"] .wb-group-title { color: var(--wb-violet); }
|
|
446
|
+
.wb-group[data-color="ink"] { border-color: var(--wb-rule); }
|
|
447
|
+
.wb-group[data-color="ink"] .wb-group-title { color: var(--wb-ink); }
|
|
448
|
+
/* The legend: a card-like glyph per entry, drawn with the card rules and
|
|
449
|
+
scaled to 0.4 inside a box of the scaled size, and what it stands for.
|
|
450
|
+
A transform, not zoom: Electron's Chromium reports a zoomed element's
|
|
451
|
+
rect in unzoomed units, which made the stage measure itself 2.5x tall. */
|
|
452
|
+
.wb-legend { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--wb-space-6) var(--wb-space-16); padding: 0 var(--wb-space-24) var(--wb-space-12); color: var(--wb-ink-muted); font: var(--wb-text-micro)/var(--wb-leading-normal) var(--wb-font-mono); letter-spacing: var(--wb-tracking-chrome); text-transform: uppercase; }
|
|
453
|
+
.wb-legend-entry { display: inline-flex; align-items: center; gap: var(--wb-space-6); white-space: nowrap; }
|
|
454
|
+
.wb-legend-box { position: relative; flex: none; width: 20px; height: 13px; }
|
|
455
|
+
.wb-card.wb-legend-glyph { position: absolute; top: 0; left: 0; display: block; width: 50px; min-width: 0; height: 32px; padding: 0; transform: scale(0.4); transform-origin: 0 0; }
|
|
456
|
+
.wb-card.wb-legend-glyph[data-form="cylinder"] { height: 44px; }
|
|
457
|
+
/* Lines that scale to a pixel: the glyph's borders, its lid and rule marks,
|
|
458
|
+
and its change stripe are drawn 2.5x heavier before the 0.4 scale. */
|
|
459
|
+
.wb-card.wb-legend-glyph, .wb-card.wb-legend-glyph::before, .wb-card.wb-legend-glyph::after { border-width: 2.5px; }
|
|
460
|
+
.wb-card.wb-legend-glyph[data-change] { border-left-width: 7px; }
|
|
335
461
|
:root[data-wb-instant] *, :root[data-wb-instant] *::before, :root[data-wb-instant] *::after {
|
|
336
462
|
transition: none !important;
|
|
337
463
|
animation-duration: 0s !important;
|
|
@@ -348,7 +474,8 @@ function stageStyleReference() {
|
|
|
348
474
|
* animation.html only for what they do not cover, written with the variables:
|
|
349
475
|
* no literal colors, sizes or font names. data-tone="accent|added|removed|
|
|
350
476
|
* modified|muted" colors any card, node, chip, badge, shape or table row;
|
|
351
|
-
* data-highlight="frameKey ..." rings a card, node or shape in those frames
|
|
477
|
+
* data-highlight="frameKey ..." rings a card, node or shape in those frames
|
|
478
|
+
* (in a clip, those markers).
|
|
352
479
|
*
|
|
353
480
|
* Classes:
|
|
354
481
|
${CLASSES.map(([selector, use]) => ` * ${selector.padEnd(26)} ${use}`).join("\n")}
|
|
@@ -363,10 +490,62 @@ ${STAGE_KIT_CSS}
|
|
|
363
490
|
`;
|
|
364
491
|
}
|
|
365
492
|
//#endregion
|
|
493
|
+
//#region src/review-api/animation-references.ts
|
|
494
|
+
/** The README's H1 and the paragraph right after it. */
|
|
495
|
+
function readmeHeading(file, text) {
|
|
496
|
+
const lines = text.split(/\r?\n/);
|
|
497
|
+
const start = lines.findIndex((line) => line.trim());
|
|
498
|
+
const title = lines[start]?.match(/^# (.+)$/)?.[1]?.trim();
|
|
499
|
+
const rest = lines.slice(start + 1);
|
|
500
|
+
const from = rest.findIndex((line) => line.trim());
|
|
501
|
+
const paragraph = [];
|
|
502
|
+
for (const line of from < 0 ? [] : rest.slice(from)) {
|
|
503
|
+
if (!line.trim() || line.startsWith("#")) break;
|
|
504
|
+
paragraph.push(line.trim());
|
|
505
|
+
}
|
|
506
|
+
const summary = paragraph.join(" ");
|
|
507
|
+
if (!title || !summary) throw new ReviewInputError(`${file} must open with "# Title" followed by a one-sentence summary paragraph.`);
|
|
508
|
+
return {
|
|
509
|
+
title,
|
|
510
|
+
summary
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/** Every reference animation, sorted by name, each checked as a publish would
|
|
514
|
+
* be. Strict (the default) throws on a broken reference, for the tests that
|
|
515
|
+
* keep them valid; otherwise a broken one is left out, so an agent's checkout
|
|
516
|
+
* and instructions never fail on a reference. */
|
|
517
|
+
async function readAnimationReferences(root = findReviewPackageRoot(import.meta.url), { strict = true } = {}) {
|
|
518
|
+
const parent = path.join(root, "instructions", "animations");
|
|
519
|
+
let entries;
|
|
520
|
+
try {
|
|
521
|
+
entries = await readdir(parent, { withFileTypes: true });
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (isMissingFileError(error)) return [];
|
|
524
|
+
throw error;
|
|
525
|
+
}
|
|
526
|
+
const names = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
527
|
+
return (await Promise.all(names.map(async (name) => {
|
|
528
|
+
const directory = path.join(parent, name);
|
|
529
|
+
const readme = path.join(directory, "README.md");
|
|
530
|
+
try {
|
|
531
|
+
await readAnimationFolder(directory);
|
|
532
|
+
return {
|
|
533
|
+
name,
|
|
534
|
+
directory,
|
|
535
|
+
...readmeHeading(readme, await readFile(readme, "utf8"))
|
|
536
|
+
};
|
|
537
|
+
} catch (error) {
|
|
538
|
+
if (!strict) return void 0;
|
|
539
|
+
throw new ReviewInputError(`Reference animation ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
540
|
+
}
|
|
541
|
+
}))).filter((reference) => reference !== void 0);
|
|
542
|
+
}
|
|
543
|
+
//#endregion
|
|
366
544
|
//#region src/review-api/animation-drafts.ts
|
|
367
545
|
const FILES = {
|
|
368
546
|
html: "animation.html",
|
|
369
547
|
frames: "frames.json",
|
|
548
|
+
markers: "markers.json",
|
|
370
549
|
scenarios: "scenarios.json",
|
|
371
550
|
block: "block.json",
|
|
372
551
|
checkout: "checkout.json"
|
|
@@ -378,10 +557,11 @@ const BLOCK_FIELDS = [
|
|
|
378
557
|
"height",
|
|
379
558
|
"pins"
|
|
380
559
|
];
|
|
381
|
-
/** Every field an animation's files can set. */
|
|
560
|
+
/** Every field an animation's or clip's files can set. */
|
|
382
561
|
const FIELDS = [
|
|
383
562
|
"html",
|
|
384
563
|
"frames",
|
|
564
|
+
"markers",
|
|
385
565
|
"scenarios",
|
|
386
566
|
...BLOCK_FIELDS
|
|
387
567
|
];
|
|
@@ -396,21 +576,60 @@ async function writeReadOnly(file, content) {
|
|
|
396
576
|
await rm(file, { force: true });
|
|
397
577
|
await writeFile(file, content, { mode: 292 });
|
|
398
578
|
}
|
|
399
|
-
|
|
579
|
+
/** Copies `source` to `target` with every file read-only, replacing what was there. */
|
|
580
|
+
async function copyReadOnly(source, target) {
|
|
581
|
+
await rm(target, {
|
|
582
|
+
recursive: true,
|
|
583
|
+
force: true
|
|
584
|
+
});
|
|
585
|
+
await cp(source, target, { recursive: true });
|
|
586
|
+
for (const entry of await readdir(target, {
|
|
587
|
+
recursive: true,
|
|
588
|
+
withFileTypes: true
|
|
589
|
+
})) if (entry.isFile()) await chmod(path.join(entry.parentPath, entry.name), 292);
|
|
590
|
+
}
|
|
591
|
+
async function writeStageReference(root = findReviewPackageRoot(import.meta.url)) {
|
|
400
592
|
const directory = path.join(devReviewHome(), "animation-drafts");
|
|
401
593
|
const styles = path.join(directory, "whiteboard.css");
|
|
402
594
|
const api = path.join(directory, "whiteboard.d.ts");
|
|
403
|
-
const
|
|
404
|
-
await
|
|
595
|
+
const copies = path.join(directory, "references");
|
|
596
|
+
const sources = await readAnimationReferences(root, { strict: false });
|
|
597
|
+
await mkdir(copies, { recursive: true });
|
|
405
598
|
await writeReadOnly(styles, stageStyleReference());
|
|
406
|
-
await writeReadOnly(api, await readFile(
|
|
599
|
+
await writeReadOnly(api, await readFile(path.join(root, "instructions", "whiteboard.d.ts"), "utf8"));
|
|
600
|
+
const names = new Set(sources.map(({ name }) => name));
|
|
601
|
+
for (const stale of await readdir(copies)) if (!names.has(stale)) await rm(path.join(copies, stale), {
|
|
602
|
+
recursive: true,
|
|
603
|
+
force: true
|
|
604
|
+
});
|
|
407
605
|
return {
|
|
408
606
|
styles,
|
|
409
|
-
api
|
|
607
|
+
api,
|
|
608
|
+
references: await Promise.all(sources.map(async (reference) => {
|
|
609
|
+
const copy = path.join(copies, reference.name);
|
|
610
|
+
await copyReadOnly(reference.directory, copy);
|
|
611
|
+
return {
|
|
612
|
+
...reference,
|
|
613
|
+
directory: copy
|
|
614
|
+
};
|
|
615
|
+
}))
|
|
410
616
|
};
|
|
411
617
|
}
|
|
412
618
|
/** The line that sends an agent to the references before it writes an animation. */
|
|
413
|
-
const stageReferenceHint = ({ styles, api }) => `Before writing or restyling an animation's HTML, read ${styles} (the classes and --wb-* variables every animation loads) and ${api} (the whiteboard API its scripts call).
|
|
619
|
+
const stageReferenceHint = ({ styles, api, references }) => `Before writing or restyling an animation's HTML, read ${styles} (the classes and --wb-* variables every animation loads) and ${api} (the whiteboard API its scripts call).${references.length ? ` Reference animations to start from are in ${path.dirname(references[0].directory)}: read the README of the closest one.` : ""} All are read-only and rewritten by Whiteboard.`;
|
|
620
|
+
/** Lists the reference animations where the agent can read them. */
|
|
621
|
+
function referencesSection({ references }) {
|
|
622
|
+
if (!references.length) return [];
|
|
623
|
+
return [[
|
|
624
|
+
"## References",
|
|
625
|
+
"",
|
|
626
|
+
"Before you write an animation, read the README of the reference closest to what you are drawing, then copy its files into your draft folder as a starting point.",
|
|
627
|
+
"",
|
|
628
|
+
...references.map(({ name, summary, directory }) => `- \`${name}\`: ${summary} ${directory}`)
|
|
629
|
+
].join("\n")];
|
|
630
|
+
}
|
|
631
|
+
/** What follows the animation instructions: the hint, then the references. */
|
|
632
|
+
const stageReferenceText = (reference) => [stageReferenceHint(reference), ...referencesSection(reference)].join("\n\n");
|
|
414
633
|
function draftDirectory(sessionId, targetId, dir) {
|
|
415
634
|
if (dir) return path.resolve(dir);
|
|
416
635
|
if (!handle.test(sessionId) || !handle.test(targetId)) throw new ReviewInputError("sessionId and targetId must be IDs.");
|
|
@@ -419,7 +638,7 @@ function draftDirectory(sessionId, targetId, dir) {
|
|
|
419
638
|
const fingerprint = (block) => createHash("sha256").update(JSON.stringify(block)).digest("hex");
|
|
420
639
|
const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
421
640
|
function animationBlock(block, targetId) {
|
|
422
|
-
if (!isJsonObject(block) || block.type !== "animation") throw new ReviewInputError(`${targetId} is ${isJsonObject(block) ? `a ${String(block.type)}` : "not a component"}; only animations check out as files.`);
|
|
641
|
+
if (!isJsonObject(block) || block.type !== "animation" && block.type !== "clip") throw new ReviewInputError(`${targetId} is ${isJsonObject(block) ? `a ${String(block.type)}` : "not a component"}; only animations and clips check out as files.`);
|
|
423
642
|
return block;
|
|
424
643
|
}
|
|
425
644
|
/** Writes the block's files, replacing any earlier draft in the folder. */
|
|
@@ -434,7 +653,8 @@ async function checkoutAnimation(read, sessionId, targetId, dir) {
|
|
|
434
653
|
await writeFile(files[key], text);
|
|
435
654
|
};
|
|
436
655
|
await put("html", String(block.html));
|
|
437
|
-
await put("
|
|
656
|
+
if (block.type === "clip") await put("markers", json(block.markers ?? []));
|
|
657
|
+
else await put("frames", json(block.frames ?? []));
|
|
438
658
|
if (block.scenarios !== void 0) await put("scenarios", json(block.scenarios));
|
|
439
659
|
await put("block", json(Object.fromEntries(Object.entries(block).filter(([key]) => BLOCK_FIELDS.includes(key)))));
|
|
440
660
|
await writeCheckout(directory, {
|
|
@@ -446,7 +666,8 @@ async function checkoutAnimation(read, sessionId, targetId, dir) {
|
|
|
446
666
|
return {
|
|
447
667
|
directory,
|
|
448
668
|
files,
|
|
449
|
-
|
|
669
|
+
styles: reference.styles,
|
|
670
|
+
api: reference.api,
|
|
450
671
|
next: `Edit these files with your file tools, then publish the folder: session_edit {sessionId, edit: {type: "update", targetId: "${targetId}", fromFiles: "${directory}"}}. ${stageReferenceHint(reference)}`
|
|
451
672
|
};
|
|
452
673
|
}
|
|
@@ -458,7 +679,7 @@ async function readText(directory, name, required) {
|
|
|
458
679
|
return await readFile(path.join(directory, name), "utf8");
|
|
459
680
|
} catch (error) {
|
|
460
681
|
if (!isMissingFileError(error)) throw error;
|
|
461
|
-
if (required) throw new ReviewInputError(`${path.join(directory, name)} does not exist. Check the animation out with session_checkout, or write the file.`);
|
|
682
|
+
if (required) throw new ReviewInputError(`${path.join(directory, name)} does not exist. Check the animation or clip out with session_checkout, or write the file.`);
|
|
462
683
|
return;
|
|
463
684
|
}
|
|
464
685
|
}
|
|
@@ -471,25 +692,40 @@ async function readJson(directory, name, required) {
|
|
|
471
692
|
throw new ReviewInputError(`${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
472
693
|
}
|
|
473
694
|
}
|
|
474
|
-
/** The animation a folder holds, as a block's fields
|
|
475
|
-
|
|
476
|
-
|
|
695
|
+
/** The animation or clip a folder holds, as a block's fields with its type,
|
|
696
|
+
* checked as a publish would be. frames.json makes it an animation and
|
|
697
|
+
* markers.json a clip. */
|
|
698
|
+
async function readAnimationFolder(directory) {
|
|
699
|
+
const [html, frames, markers, scenarios, block] = await Promise.all([
|
|
477
700
|
readText(directory, FILES.html, true),
|
|
478
|
-
readJson(directory, FILES.frames,
|
|
701
|
+
readJson(directory, FILES.frames, false),
|
|
702
|
+
readJson(directory, FILES.markers, false),
|
|
479
703
|
readJson(directory, FILES.scenarios, false),
|
|
480
|
-
readJson(directory, FILES.block, true)
|
|
481
|
-
readJson(directory, FILES.checkout, false)
|
|
704
|
+
readJson(directory, FILES.block, true)
|
|
482
705
|
]);
|
|
706
|
+
if (frames !== void 0 && markers !== void 0) throw new ReviewInputError(`${directory} holds both ${FILES.frames} and ${FILES.markers}. Keep ${FILES.frames} for an animation or ${FILES.markers} for a clip, not both.`);
|
|
707
|
+
if (frames === void 0 && markers === void 0) throw new ReviewInputError(`${path.join(directory, FILES.frames)} does not exist. Write ${FILES.frames} for an animation or ${FILES.markers} for a clip, or check one out with session_checkout.`);
|
|
483
708
|
if (!isJsonObject(block)) throw new ReviewInputError(`${FILES.block} must hold a JSON object.`);
|
|
484
709
|
const stray = Object.keys(block).filter((key) => !BLOCK_FIELDS.includes(key));
|
|
485
|
-
if (stray.length) throw new ReviewInputError(`${FILES.block} holds only ${BLOCK_FIELDS.join(", ")}; move ${stray.join(", ")} out (html is ${FILES.html}, frames ${FILES.frames}, scenarios ${FILES.scenarios}).`);
|
|
486
|
-
const fields = {
|
|
710
|
+
if (stray.length) throw new ReviewInputError(`${FILES.block} holds only ${BLOCK_FIELDS.join(", ")}; move ${stray.join(", ")} out (html is ${FILES.html}, frames ${FILES.frames} or markers ${FILES.markers}, scenarios ${FILES.scenarios}).`);
|
|
711
|
+
const fields = markers === void 0 ? {
|
|
487
712
|
...block,
|
|
713
|
+
type: "animation",
|
|
488
714
|
html,
|
|
489
715
|
frames
|
|
716
|
+
} : {
|
|
717
|
+
...block,
|
|
718
|
+
type: "clip",
|
|
719
|
+
html,
|
|
720
|
+
markers
|
|
490
721
|
};
|
|
491
722
|
if (scenarios !== void 0) fields.scenarios = scenarios;
|
|
492
723
|
validate(fields);
|
|
724
|
+
return fields;
|
|
725
|
+
}
|
|
726
|
+
/** The animation a folder holds, as a block's fields, and where it came from. */
|
|
727
|
+
async function readDraft(directory) {
|
|
728
|
+
const [fields, checkout] = await Promise.all([readAnimationFolder(directory), readJson(directory, FILES.checkout, false)]);
|
|
493
729
|
const parsed = checkoutSchema.safeParse(checkout);
|
|
494
730
|
return {
|
|
495
731
|
fields,
|
|
@@ -501,24 +737,21 @@ function located(issuePath) {
|
|
|
501
737
|
const [field, ...rest] = issuePath.map(String);
|
|
502
738
|
const at = rest.map((part) => /^\d+$/.test(part) ? `[${part}]` : `.${part}`).join("");
|
|
503
739
|
if (field === "html") return FILES.html;
|
|
504
|
-
if (field === "frames" || field === "scenarios") return `${FILES[field]} ${at.replace(/^\./, "") || "(top level)"}`;
|
|
740
|
+
if (field === "frames" || field === "markers" || field === "scenarios") return `${FILES[field]} ${at.replace(/^\./, "") || "(top level)"}`;
|
|
505
741
|
return `${FILES.block} ${field ?? ""}${at}`;
|
|
506
742
|
}
|
|
507
743
|
/** Checks the files before anything is sent, so an issue names its file. The
|
|
508
744
|
* host checks pins: agents write them without the repository it fills in. */
|
|
509
745
|
function validate(fields) {
|
|
510
746
|
const { pins: _pins, ...rest } = fields;
|
|
511
|
-
const parsed = blocks.
|
|
512
|
-
...rest,
|
|
513
|
-
type: "animation"
|
|
514
|
-
});
|
|
747
|
+
const parsed = blocks[fields.type].schema.safeParse(rest);
|
|
515
748
|
if (!parsed.success) throw new ReviewInputError(parsed.error.issues.map((issue) => `${located(issue.path)}: ${issue.message}`).join("\n"));
|
|
516
749
|
checkReferences([parsed.data]);
|
|
517
750
|
}
|
|
518
751
|
/**
|
|
519
752
|
* Turns an edit that names a draft folder into the edit the host takes: the
|
|
520
|
-
* folder's animation as an insert's or replace's content, or as an
|
|
521
|
-
* changed fields. An update from a checkout is refused once the block has
|
|
753
|
+
* folder's animation or clip as an insert's or replace's content, or as an
|
|
754
|
+
* update's changed fields. An update from a checkout is refused once the block has
|
|
522
755
|
* changed since. Returns `unchanged` when the files match the block.
|
|
523
756
|
*/
|
|
524
757
|
async function resolveDraftEdit(read, sessionId, edit) {
|
|
@@ -531,10 +764,7 @@ async function resolveDraftEdit(read, sessionId, edit) {
|
|
|
531
764
|
directory,
|
|
532
765
|
edit: {
|
|
533
766
|
...rest,
|
|
534
|
-
content:
|
|
535
|
-
type: "animation",
|
|
536
|
-
...fields
|
|
537
|
-
}
|
|
767
|
+
content: fields
|
|
538
768
|
}
|
|
539
769
|
};
|
|
540
770
|
const targetId = String(rest.targetId);
|
|
@@ -544,13 +774,11 @@ async function resolveDraftEdit(read, sessionId, edit) {
|
|
|
544
774
|
directory,
|
|
545
775
|
edit: {
|
|
546
776
|
...rest,
|
|
547
|
-
content:
|
|
548
|
-
type: "animation",
|
|
549
|
-
...fields
|
|
550
|
-
}
|
|
777
|
+
content: fields
|
|
551
778
|
}
|
|
552
779
|
};
|
|
553
780
|
if (rest.type !== "update") throw new ReviewInputError("fromFiles publishes an insert, replace or update.");
|
|
781
|
+
if (current.type !== fields.type) throw new ReviewInputError(`${directory} holds ${fields.type === "clip" ? "a clip" : "an animation"} but ${targetId} is ${current.type === "clip" ? "a clip" : "an animation"}. Publish it with type "replace" to change its kind.`);
|
|
554
782
|
const changes = {};
|
|
555
783
|
for (const field of FIELDS) if (JSON.stringify(fields[field]) !== JSON.stringify(current[field])) changes[field] = fields[field] ?? null;
|
|
556
784
|
return Object.keys(changes).length ? {
|
|
@@ -634,7 +862,7 @@ async function callPublicTool(client, tool, input, signal) {
|
|
|
634
862
|
};
|
|
635
863
|
if (tool.name === "session_get_instructions" && rest.topic === "animation") {
|
|
636
864
|
const instructions = await callAuthoringTool(client, tool, fields, signal);
|
|
637
|
-
const hint =
|
|
865
|
+
const hint = stageReferenceText(await writeStageReference());
|
|
638
866
|
if (instructions instanceof ToolText) return new ToolText(`${instructions.text}\n\n${hint}`);
|
|
639
867
|
return isStringValue(instructions) ? `${instructions}\n\n${hint}` : instructions;
|
|
640
868
|
}
|
|
@@ -649,9 +877,45 @@ async function callPublicTool(client, tool, input, signal) {
|
|
|
649
877
|
const result = await callAuthoringTool(client, tool, fields, signal);
|
|
650
878
|
if (result instanceof ToolText) return result;
|
|
651
879
|
const published = withoutRepositoryIds(publicResult(result));
|
|
880
|
+
if (tool.name === "session_render" && isStringValue(sessionId) && isStringValue(rest.targetId)) return saveRenderedFrame(published, sessionId, rest.targetId);
|
|
652
881
|
if (draft && isStringValue(sessionId) && isJsonObject(published) && isStringValue(published.targetId)) await recordPublish(read, sessionId, published.targetId, draft.directory).catch(() => {});
|
|
653
882
|
return published;
|
|
654
883
|
}
|
|
884
|
+
/** A rendered frame's PNG, for an agent that reads images from a tool
|
|
885
|
+
* result, and the rest of the result for its text. */
|
|
886
|
+
function renderedImage(tool, result) {
|
|
887
|
+
if (tool.name !== "session_render" || result instanceof ToolText || !isJsonObject(result) || !isStringValue(result.png)) return { result };
|
|
888
|
+
const { png, ...rest } = result;
|
|
889
|
+
return {
|
|
890
|
+
image: {
|
|
891
|
+
data: png,
|
|
892
|
+
mimeType: "image/png"
|
|
893
|
+
},
|
|
894
|
+
result: rest
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
/** Writes the frame's PNG where the agent can open it with its own tools,
|
|
898
|
+
* beside the animation drafts, and names the file in the result. A folder
|
|
899
|
+
* that cannot be written leaves the image in the result alone. */
|
|
900
|
+
async function saveRenderedFrame(result, sessionId, targetId) {
|
|
901
|
+
if (!isJsonObject(result) || !isStringValue(result.png)) return result;
|
|
902
|
+
const key = isJsonObject(result.frame) && isStringValue(result.frame.key) ? result.frame.key : "frame";
|
|
903
|
+
const file = path.join(devReviewHome(), "animation-renders", ...[
|
|
904
|
+
sessionId,
|
|
905
|
+
targetId,
|
|
906
|
+
`${key}.png`
|
|
907
|
+
].map((part) => part.replace(/[^\w.-]/g, "_")));
|
|
908
|
+
try {
|
|
909
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
910
|
+
await writeFile(file, Buffer.from(result.png, "base64"));
|
|
911
|
+
} catch {
|
|
912
|
+
return result;
|
|
913
|
+
}
|
|
914
|
+
return {
|
|
915
|
+
...result,
|
|
916
|
+
file
|
|
917
|
+
};
|
|
918
|
+
}
|
|
655
919
|
/** The read a draft compares and records against: session_get's JSON. */
|
|
656
920
|
const INSPECT = {
|
|
657
921
|
name: "review_get",
|
|
@@ -712,4 +976,4 @@ function toolFailure(error, phase) {
|
|
|
712
976
|
};
|
|
713
977
|
}
|
|
714
978
|
//#endregion
|
|
715
|
-
export { publicTool as i, RECOVERY as n, callPublicTool as r, toolFailure as t };
|
|
979
|
+
export { renderedImage as a, publicTool as i, RECOVERY as n, callPublicTool as r, toolFailure as t };
|