@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
|
@@ -21,6 +21,10 @@ export const STAGE_THEME = {
|
|
|
21
21
|
"added-bg": "--diff-added-bg",
|
|
22
22
|
"removed-bg": "--diff-removed-bg",
|
|
23
23
|
"modified-bg": "--diff-modified-bg",
|
|
24
|
+
// Two more outline colours for telling kinds of card apart: the software
|
|
25
|
+
// map's storage colour, and the merged purple.
|
|
26
|
+
teal: "--map-storage-border",
|
|
27
|
+
violet: "--merged",
|
|
24
28
|
// Each face by its role: the chrome's for labels, mono for what a
|
|
25
29
|
// terminal would print, the reading face for prose, and the code font.
|
|
26
30
|
"font-sans": "--chrome-font",
|
|
@@ -52,6 +56,8 @@ export const STAGE_VARIABLES: [note: string, names: string[]][] = [
|
|
|
52
56
|
"added-bg",
|
|
53
57
|
"removed-bg",
|
|
54
58
|
"modified-bg",
|
|
59
|
+
"teal",
|
|
60
|
+
"violet",
|
|
55
61
|
],
|
|
56
62
|
],
|
|
57
63
|
[
|
|
@@ -149,7 +155,7 @@ const CLASSES: [selector: string, use: string][] = [
|
|
|
149
155
|
],
|
|
150
156
|
[
|
|
151
157
|
".wb-card",
|
|
152
|
-
"A box with a title and note:
|
|
158
|
+
"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.",
|
|
153
159
|
],
|
|
154
160
|
[
|
|
155
161
|
".wb-node",
|
|
@@ -178,6 +184,18 @@ const CLASSES: [selector: string, use: string][] = [
|
|
|
178
184
|
".wb-shape, .wb-shape-text",
|
|
179
185
|
"An SVG shape drawn like a card, and the text inside it.",
|
|
180
186
|
],
|
|
187
|
+
[
|
|
188
|
+
".wb-group",
|
|
189
|
+
"A washed box behind a scene's cards with a .wb-group-title; scene.group draws one.",
|
|
190
|
+
],
|
|
191
|
+
[
|
|
192
|
+
".wb-legend",
|
|
193
|
+
"A row of .wb-legend-entry glyphs and labels under a scene; scene.legend draws one.",
|
|
194
|
+
],
|
|
195
|
+
[
|
|
196
|
+
"[data-wb-paint]",
|
|
197
|
+
'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.',
|
|
198
|
+
],
|
|
181
199
|
];
|
|
182
200
|
|
|
183
201
|
/** The CSS every stage loads before its own markup. */
|
|
@@ -186,6 +204,7 @@ export const STAGE_KIT_CSS = `:root { --wb-duration: 450ms; color-scheme: light
|
|
|
186
204
|
button { color: inherit; font: inherit; }
|
|
187
205
|
html, body { margin: 0; height: 100%; }
|
|
188
206
|
html[data-wb-auto-size] body { height: auto; display: flow-root; }
|
|
207
|
+
html[data-wb-unpainted] body { visibility: hidden; }
|
|
189
208
|
body {
|
|
190
209
|
overflow: hidden;
|
|
191
210
|
background: transparent;
|
|
@@ -240,9 +259,76 @@ body {
|
|
|
240
259
|
.wb-node { min-width: 88px; text-align: center; }
|
|
241
260
|
.wb-node small { display: block; color: var(--wb-ink-faint); font-size: var(--wb-text-micro); }
|
|
242
261
|
.wb-card-head { display: flex; align-items: center; gap: var(--wb-space-6); }
|
|
243
|
-
|
|
262
|
+
/* The dot says what state a card is in: it shows only with a tone or a
|
|
263
|
+
change mark, in that colour, so a plain card carries no unexplained mark. */
|
|
264
|
+
.wb-card-dot { display: none; flex: none; width: 7px; height: 7px; border-radius: var(--wb-radius-round); background: var(--wb-ink-faint); }
|
|
265
|
+
.wb-card:is([data-tone], [data-change]) .wb-card-dot { display: block; }
|
|
244
266
|
.wb-card-title { min-width: 0; overflow-wrap: anywhere; font-weight: var(--wb-weight-semibold); }
|
|
245
267
|
.wb-card-note { color: var(--wb-ink-muted); overflow-wrap: anywhere; font-size: var(--wb-text-micro); min-height: 12px; }
|
|
268
|
+
|
|
269
|
+
/* A store's text sits in the middle of its drum or cylinder, as a label on
|
|
270
|
+
a tank does; a pill's too. */
|
|
271
|
+
:where(.wb-card, .wb-node):is([data-form="cylinder"], [data-form="drum"], [data-form="pill"]) { align-items: center; text-align: center; }
|
|
272
|
+
.wb-card:is([data-form="cylinder"], [data-form="drum"], [data-form="pill"]) :is(.wb-card-head, .wb-items) { justify-content: center; }
|
|
273
|
+
/* Shapes: a card's or node's silhouette says what it stands for before
|
|
274
|
+
its title does. data-form is box (the default), pill, cut (a branch),
|
|
275
|
+
cylinder (a database), drum (a cache), stack (a queue), window (a
|
|
276
|
+
client) or bars (a service of this codebase). data-outline is solid,
|
|
277
|
+
dashed (outside the system) or dotted. data-color is the resting border
|
|
278
|
+
colour: ink, accent, teal or violet; a tone takes over while set. The
|
|
279
|
+
scene's cards take these as options. These rules and the change marks
|
|
280
|
+
weigh one class, as the card rules do: a hand-made layout's own rule for
|
|
281
|
+
its nodes, say position: absolute, comes later and wins. A tone or a
|
|
282
|
+
highlight still takes over. */
|
|
283
|
+
:where(.wb-card, .wb-node)[data-form] { position: relative; }
|
|
284
|
+
:where(.wb-card, .wb-node)[data-form]::before, :where(.wb-card, .wb-node)[data-form]::after { pointer-events: none; }
|
|
285
|
+
:where(.wb-card, .wb-node)[data-form="pill"] { padding-inline: var(--wb-space-16); border-radius: var(--wb-radius-pill); }
|
|
286
|
+
:where(.wb-card, .wb-node)[data-form="cut"] { border-radius: var(--wb-space-12); corner-shape: bevel; }
|
|
287
|
+
: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); }
|
|
288
|
+
:where(.wb-card, .wb-node)[data-form="cylinder"]::before {
|
|
289
|
+
content: ""; position: absolute; inset: -1px -1px auto; height: var(--wb-space-20);
|
|
290
|
+
border: 1px solid; border-color: inherit; border-radius: 50%; background: inherit;
|
|
291
|
+
}
|
|
292
|
+
: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%; }
|
|
293
|
+
:where(.wb-card, .wb-node)[data-form="drum"]::before {
|
|
294
|
+
content: ""; position: absolute; inset: -1px auto -1px -1px; width: var(--wb-space-24);
|
|
295
|
+
border: 1px solid; border-color: inherit; border-radius: 50%; background: inherit;
|
|
296
|
+
}
|
|
297
|
+
:where(.wb-card, .wb-node)[data-form="stack"] {
|
|
298
|
+
margin-right: var(--wb-space-8); margin-bottom: var(--wb-space-8);
|
|
299
|
+
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);
|
|
300
|
+
}
|
|
301
|
+
:where(.wb-card, .wb-node)[data-form="window"] { padding-top: var(--wb-space-16); }
|
|
302
|
+
: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; }
|
|
303
|
+
:where(.wb-card, .wb-node)[data-form="bars"] { padding-inline: var(--wb-space-18); }
|
|
304
|
+
: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; }
|
|
305
|
+
:where(.wb-card, .wb-node)[data-form="bars"]::before { left: var(--wb-space-6); }
|
|
306
|
+
:where(.wb-card, .wb-node)[data-form="bars"]::after { right: var(--wb-space-6); }
|
|
307
|
+
/* What the change under review did to a part: its dot and a stripe down its
|
|
308
|
+
left edge in the change colour, at rest; a removed part's title is struck.
|
|
309
|
+
A frame's tone still colours the whole card over it. */
|
|
310
|
+
:where(.wb-card, .wb-node)[data-change] { border-left-width: 3px; }
|
|
311
|
+
:where(.wb-card, .wb-node)[data-change="added"] { border-left-color: var(--wb-added); }
|
|
312
|
+
:where(.wb-card, .wb-node)[data-change="modified"] { border-left-color: var(--wb-modified); }
|
|
313
|
+
:where(.wb-card, .wb-node)[data-change="removed"] { border-left-color: var(--wb-removed); }
|
|
314
|
+
.wb-card[data-change="added"] .wb-card-dot { background: var(--wb-added); }
|
|
315
|
+
.wb-card[data-change="modified"] .wb-card-dot { background: var(--wb-modified); }
|
|
316
|
+
.wb-card[data-change="removed"] .wb-card-dot { background: var(--wb-removed); }
|
|
317
|
+
.wb-card[data-change="removed"] .wb-card-title { color: var(--wb-ink-muted); text-decoration: line-through; }
|
|
318
|
+
:where(.wb-card, .wb-node)[data-outline="dashed"] { border-style: dashed; }
|
|
319
|
+
:where(.wb-card, .wb-node)[data-outline="dotted"] { border-style: dotted; }
|
|
320
|
+
:where(.wb-card, .wb-node)[data-color="ink"] { border-color: var(--wb-ink); }
|
|
321
|
+
:where(.wb-card, .wb-node)[data-color="accent"] { border-color: var(--wb-accent); }
|
|
322
|
+
:where(.wb-card, .wb-node)[data-color="teal"] { border-color: var(--wb-teal); }
|
|
323
|
+
:where(.wb-card, .wb-node)[data-color="violet"] { border-color: var(--wb-violet); }
|
|
324
|
+
.wb-card[data-color="ink"] .wb-card-dot { background: var(--wb-ink); }
|
|
325
|
+
.wb-card[data-color="accent"] .wb-card-dot { background: var(--wb-accent); }
|
|
326
|
+
.wb-card[data-color="teal"] .wb-card-dot { background: var(--wb-teal); }
|
|
327
|
+
.wb-card[data-color="violet"] .wb-card-dot { background: var(--wb-violet); }
|
|
328
|
+
.wb-shape[data-color="ink"] { stroke: var(--wb-ink); }
|
|
329
|
+
.wb-shape[data-color="accent"] { stroke: var(--wb-accent); }
|
|
330
|
+
.wb-shape[data-color="teal"] { stroke: var(--wb-teal); }
|
|
331
|
+
.wb-shape[data-color="violet"] { stroke: var(--wb-violet); }
|
|
246
332
|
.wb-well { padding: var(--wb-space-12); border: 1px solid var(--wb-rule-soft); border-radius: var(--wb-radius-surface); background: var(--wb-well); }
|
|
247
333
|
.wb-well :is(.wb-item, .wb-chip, .wb-badge, .wb-code) { background: var(--wb-surface); }
|
|
248
334
|
.wb-rule { margin: var(--wb-space-12) 0; border: 0; border-top: 1px solid var(--wb-rule); }
|
|
@@ -306,10 +392,15 @@ body {
|
|
|
306
392
|
.wb-shape[data-tone="modified"] { stroke: var(--wb-modified); fill: var(--wb-modified-bg); }
|
|
307
393
|
.wb-shape[data-tone="muted"] { stroke-dasharray: 4 3; }
|
|
308
394
|
:is(.wb-card, .wb-node)[data-wb-highlit] { border-color: var(--wb-accent); box-shadow: 0 0 0 3px var(--wb-accent-soft); }
|
|
395
|
+
:is(.wb-card, .wb-node)[data-form="stack"][data-wb-highlit] {
|
|
396
|
+
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);
|
|
397
|
+
}
|
|
309
398
|
.wb-shape[data-wb-highlit] { stroke: var(--wb-accent); stroke-width: 2; }
|
|
310
399
|
|
|
311
400
|
/* Connectors and tokens. */
|
|
312
|
-
|
|
401
|
+
/* Above the cards, which sit above their groups: an edge's head meets a
|
|
402
|
+
card's border, and a chip stays readable as it passes one. */
|
|
403
|
+
.wb-arrows, .wb-tokens { position: fixed; inset: 0; z-index: 2; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
|
|
313
404
|
.wb-line {
|
|
314
405
|
fill: none;
|
|
315
406
|
stroke: var(--wb-ink-faint);
|
|
@@ -328,6 +419,17 @@ body {
|
|
|
328
419
|
.wb-arrow[data-tone="removed"] .wb-line { stroke: var(--wb-removed); marker-end: url(#wb-head-removed); }
|
|
329
420
|
.wb-arrow[data-tone="modified"] .wb-line { stroke: var(--wb-modified); marker-end: url(#wb-head-modified); }
|
|
330
421
|
.wb-arrow[data-wb-headless] .wb-line { marker-end: none; }
|
|
422
|
+
/* An edge carrying one of the frame's messages takes the marker, label too. */
|
|
423
|
+
.wb-arrow[data-wb-sent] .wb-line { stroke: var(--wb-accent); marker-end: url(#wb-head-accent); }
|
|
424
|
+
.wb-arrow[data-wb-sent] .wb-label { fill: var(--wb-accent); }
|
|
425
|
+
.wb-arrow[data-wb-sent="added"] .wb-line { stroke: var(--wb-added); marker-end: url(#wb-head-added); }
|
|
426
|
+
.wb-arrow[data-wb-sent="added"] .wb-label { fill: var(--wb-added); }
|
|
427
|
+
.wb-arrow[data-wb-sent="removed"] .wb-line { stroke: var(--wb-removed); marker-end: url(#wb-head-removed); }
|
|
428
|
+
.wb-arrow[data-wb-sent="removed"] .wb-label { fill: var(--wb-removed); }
|
|
429
|
+
.wb-arrow[data-wb-sent="modified"] .wb-line { stroke: var(--wb-modified); marker-end: url(#wb-head-modified); }
|
|
430
|
+
.wb-arrow[data-wb-sent="modified"] .wb-label { fill: var(--wb-modified); }
|
|
431
|
+
.wb-arrow[data-wb-sent="muted"] .wb-line { stroke: var(--wb-ink-faint); marker-end: url(#wb-head); }
|
|
432
|
+
.wb-arrow[data-wb-sent="muted"] .wb-label { fill: var(--wb-ink-muted); }
|
|
331
433
|
.wb-label {
|
|
332
434
|
fill: var(--wb-ink-muted);
|
|
333
435
|
font: var(--wb-text-micro) var(--wb-font-mono);
|
|
@@ -362,16 +464,53 @@ body {
|
|
|
362
464
|
.wb-token[data-labelled][data-tone="muted"] { color: var(--wb-token-muted-ink); }
|
|
363
465
|
|
|
364
466
|
/* The scene. */
|
|
365
|
-
.wb-scene { display: grid; gap: var(--wb-space-28) var(--wb-space-48); padding: var(--wb-space-24); align-items:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
.
|
|
467
|
+
.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; }
|
|
468
|
+
/* A card is as wide as what it says, within bounds, and sits mid-column, so
|
|
469
|
+
a wide column does not stretch it into a slab. A table keeps its column;
|
|
470
|
+
a scene's own CSS may set any width. The scene sets its own min-width from
|
|
471
|
+
the columns its cards make, and the block scrolls it sideways when
|
|
472
|
+
narrower. */
|
|
473
|
+
.wb-scene > .wb-card:not([data-kind="table"]) { justify-self: center; width: fit-content; min-width: 140px; max-width: 300px; }
|
|
371
474
|
.wb-scene > [data-wb-state="before"], .wb-scene > [data-wb-state="after"],
|
|
372
475
|
.wb-items > [data-wb-state="before"], .wb-items > [data-wb-state="after"],
|
|
373
476
|
.wb-table tr[data-wb-state="before"], .wb-table tr[data-wb-state="after"] { display: none; }
|
|
374
477
|
.wb-scene-top { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--wb-space-8); margin-bottom: var(--wb-space-4); }
|
|
478
|
+
/* A group is a washed box behind the cards of its cells, bleeding into the
|
|
479
|
+
gaps around them, titled in its top-left. Cards paint over it. */
|
|
480
|
+
.wb-scene > .wb-card { position: relative; z-index: 1; }
|
|
481
|
+
/* Groups bleed into the gaps around their cells, so a grouped scene opens
|
|
482
|
+
its rows and columns to keep the boxes apart and give edges between
|
|
483
|
+
layers room to be read. The top bleed holds the title clear of the cards
|
|
484
|
+
below it, ring and all. */
|
|
485
|
+
.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); }
|
|
486
|
+
.wb-group {
|
|
487
|
+
position: relative; z-index: 0; align-self: stretch; justify-self: stretch;
|
|
488
|
+
margin: calc(-1 * var(--wb-space-28)) calc(-1 * var(--wb-space-16)) calc(-1 * var(--wb-space-10));
|
|
489
|
+
border: 1px solid var(--wb-rule-soft); border-radius: var(--wb-radius-surface); background: var(--wb-well);
|
|
490
|
+
transition: opacity var(--wb-duration) ease;
|
|
491
|
+
}
|
|
492
|
+
.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; }
|
|
493
|
+
.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); }
|
|
494
|
+
.wb-group[data-color="accent"] .wb-group-title { color: var(--wb-accent); }
|
|
495
|
+
.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); }
|
|
496
|
+
.wb-group[data-color="teal"] .wb-group-title { color: var(--wb-teal); }
|
|
497
|
+
.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); }
|
|
498
|
+
.wb-group[data-color="violet"] .wb-group-title { color: var(--wb-violet); }
|
|
499
|
+
.wb-group[data-color="ink"] { border-color: var(--wb-rule); }
|
|
500
|
+
.wb-group[data-color="ink"] .wb-group-title { color: var(--wb-ink); }
|
|
501
|
+
/* The legend: a card-like glyph per entry, drawn with the card rules and
|
|
502
|
+
scaled to 0.4 inside a box of the scaled size, and what it stands for.
|
|
503
|
+
A transform, not zoom: Electron's Chromium reports a zoomed element's
|
|
504
|
+
rect in unzoomed units, which made the stage measure itself 2.5x tall. */
|
|
505
|
+
.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; }
|
|
506
|
+
.wb-legend-entry { display: inline-flex; align-items: center; gap: var(--wb-space-6); white-space: nowrap; }
|
|
507
|
+
.wb-legend-box { position: relative; flex: none; width: 20px; height: 13px; }
|
|
508
|
+
.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; }
|
|
509
|
+
.wb-card.wb-legend-glyph[data-form="cylinder"] { height: 44px; }
|
|
510
|
+
/* Lines that scale to a pixel: the glyph's borders, its lid and rule marks,
|
|
511
|
+
and its change stripe are drawn 2.5x heavier before the 0.4 scale. */
|
|
512
|
+
.wb-card.wb-legend-glyph, .wb-card.wb-legend-glyph::before, .wb-card.wb-legend-glyph::after { border-width: 2.5px; }
|
|
513
|
+
.wb-card.wb-legend-glyph[data-change] { border-left-width: 7px; }
|
|
375
514
|
:root[data-wb-instant] *, :root[data-wb-instant] *::before, :root[data-wb-instant] *::after {
|
|
376
515
|
transition: none !important;
|
|
377
516
|
animation-duration: 0s !important;
|
|
@@ -397,7 +536,8 @@ export function stageStyleReference(): string {
|
|
|
397
536
|
* animation.html only for what they do not cover, written with the variables:
|
|
398
537
|
* no literal colors, sizes or font names. data-tone="accent|added|removed|
|
|
399
538
|
* modified|muted" colors any card, node, chip, badge, shape or table row;
|
|
400
|
-
* data-highlight="frameKey ..." rings a card, node or shape in those frames
|
|
539
|
+
* data-highlight="frameKey ..." rings a card, node or shape in those frames
|
|
540
|
+
* (in a clip, those markers).
|
|
401
541
|
*
|
|
402
542
|
* Classes:
|
|
403
543
|
${classes}
|
package/src/cli-runner.ts
CHANGED
|
@@ -1226,7 +1226,10 @@ export async function runReviewCli(input: ReviewCliInput): Promise<number> {
|
|
|
1226
1226
|
program.command("login").description("Log in to Whiteboard with GitHub"),
|
|
1227
1227
|
"plain",
|
|
1228
1228
|
)
|
|
1229
|
-
.option(
|
|
1229
|
+
.option(
|
|
1230
|
+
"--origin <url>",
|
|
1231
|
+
`Whiteboard service origin (default: $DEV_REVIEW_SERVICE_ORIGIN or ${DEFAULT_STORE_ORIGIN})`,
|
|
1232
|
+
)
|
|
1230
1233
|
.option("--traces", "Also authorize GitHub repositories for hosted traces")
|
|
1231
1234
|
.option("--no-browser", "Print the URL instead of opening a browser")
|
|
1232
1235
|
.action(
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "block-1",
|
|
4
|
+
"type": "clip",
|
|
5
|
+
"title": "Order status",
|
|
6
|
+
"height": 300,
|
|
7
|
+
"markers": [
|
|
8
|
+
{ "key": "sent", "title": "Order sent" },
|
|
9
|
+
{
|
|
10
|
+
"key": "queued",
|
|
11
|
+
"title": "Status queued",
|
|
12
|
+
"caption": "The status line sets it.",
|
|
13
|
+
"code": ["status"]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"code": { "status": "head/order.ts#L1" },
|
|
17
|
+
"html": "<div class=\"wb-row\"><div class=\"wb-node\" id=\"order\">order</div></div><div data-wb-code=\"status\"></div><script>whiteboard.clip().marker(\"sent\").add(\"#order\", { translateX: [0, 120], duration: 800 }).marker(\"queued\");</script>"
|
|
18
|
+
}
|
|
19
|
+
]
|
package/src/review-api/README.md
CHANGED
|
@@ -55,6 +55,7 @@ All paths below are relative to `/reviews-api`.
|
|
|
55
55
|
| `GET /:id/history` | Saved versions with titles, timestamps and `author`: `"you"` for a person's save, else `"agent"` with the Ask `threadId` when one made it |
|
|
56
56
|
| `GET /:id/inspect` | Agent reading view: nested text outline with IDs; `targetId` reads one component completely, `full=true` includes all content, `version` selects history. `format=json` returns raw data instead. |
|
|
57
57
|
| `POST /:id/open` | Open the review in the attached Desktop; report an error when none is attached |
|
|
58
|
+
| `GET /:id/render?targetId=anim-1&frame=a2` | Show that frame (or a clip at that marker) of the review open in the attached Desktop and return it: `type`, `frame` (key, title, position, count), `scenario`, `clipped`, `error` (what the stage's script threw, or null) and `png` (base64). 409 when no Desktop shows the review; 400 for a frame the block does not have |
|
|
58
59
|
| `POST /commands` | Apply one command; return review ID, version, and for an edit the target ID, its `type`, and — after an insert or replace — `children`: its first-level children as `{id,type}` (a container's blocks; a diagram's steps, or nodes then edges), so new components are addressable without a read. A `create` with `pullRequestUrl` returns the newest existing review for that PR instead (owner/repository matched case-insensitively) unless `operation.reuseExisting` is `false`: `created:false`, a `note`, its stored `target`, `headMoved`, and `working`/`otherReviewIds` when they apply; its target is never moved. A new review reports `created:true`. Either way the result carries `review`, the review's `GET /` catalog entry (target, origin, repository name and path). An interactive `create` also opens the new review in an attached Desktop unless `operation.open` is `false`, and reports `opened` with the open result or an `openError`; the review is saved either way |
|
|
59
60
|
| `POST /commands {operation:{type:"lens_edit",reviewId,edit}}` | Write one file lens, credited to `activityId` when given: `insert {title,targets,afterId?}` (host id `lens-N`), `update {targetId,title?,targets?}` or `remove {targetId}`. Returns `{targetId, type:"lens", uncategorized}`, where `uncategorized` lists changed files and ranges no lens covers yet (at most 50 files) |
|
|
60
61
|
| `POST /:id/person-edit {baseVersion,title,document}` | Save a person's whole board as their version; return `{reviewId,version,ids}`. See Person edits |
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "./agent-client.js";
|
|
19
19
|
import { type ReviewApiClient, ReviewApiError } from "./client.js";
|
|
20
20
|
import { exitOnInstallUpdate } from "./install-update.js";
|
|
21
|
-
import { callPublicTool, publicTool } from "./public-tools.js";
|
|
21
|
+
import { callPublicTool, publicTool, renderedImage } from "./public-tools.js";
|
|
22
22
|
import { RECOVERY } from "./recovery.js";
|
|
23
23
|
import {
|
|
24
24
|
ASK_THREAD_ENV,
|
|
@@ -212,7 +212,8 @@ export async function runReviewAgentCli(input: AgentCliInput): Promise<number> {
|
|
|
212
212
|
durationMs: Date.now() - startedAt,
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
-
|
|
215
|
+
// A rendered frame is read from its file; its bytes stay out of stdout.
|
|
216
|
+
const text = toolResultText(tool, renderedImage(tool, result).result);
|
|
216
217
|
input.stdout.write(text.endsWith("\n") ? text : text + "\n");
|
|
217
218
|
|
|
218
219
|
return 0;
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
// An animation checked out as files, so an agent edits it with its
|
|
2
|
-
// tools and publishes the folder: the bytes travel by path, not
|
|
3
|
-
// agent's output.
|
|
1
|
+
// An animation or clip checked out as files, so an agent edits it with its
|
|
2
|
+
// own file tools and publishes the folder: the bytes travel by path, not
|
|
3
|
+
// through the agent's output. A clip's folder holds markers.json where an
|
|
4
|
+
// animation's holds frames.json.
|
|
4
5
|
|
|
5
6
|
import { createHash } from "node:crypto";
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
chmod,
|
|
9
|
+
cp,
|
|
10
|
+
mkdir,
|
|
11
|
+
readFile,
|
|
12
|
+
readdir,
|
|
13
|
+
rm,
|
|
14
|
+
writeFile,
|
|
15
|
+
} from "node:fs/promises";
|
|
7
16
|
import path from "node:path";
|
|
8
17
|
|
|
9
18
|
import {
|
|
@@ -24,9 +33,15 @@ import { isMissingFileError } from "@review/fs-utils.js";
|
|
|
24
33
|
import { findReviewPackageRoot } from "@review/package-paths.js";
|
|
25
34
|
import { z } from "zod";
|
|
26
35
|
|
|
36
|
+
import {
|
|
37
|
+
type AnimationReference,
|
|
38
|
+
readAnimationReferences,
|
|
39
|
+
} from "./animation-references.js";
|
|
40
|
+
|
|
27
41
|
const FILES = {
|
|
28
42
|
html: "animation.html",
|
|
29
43
|
frames: "frames.json",
|
|
44
|
+
markers: "markers.json",
|
|
30
45
|
scenarios: "scenarios.json",
|
|
31
46
|
block: "block.json",
|
|
32
47
|
checkout: "checkout.json",
|
|
@@ -35,8 +50,11 @@ const FILES = {
|
|
|
35
50
|
/** The fields block.json holds: everything but the ones with files of their own. */
|
|
36
51
|
const BLOCK_FIELDS = ["title", "code", "height", "pins"];
|
|
37
52
|
|
|
38
|
-
/** Every field an animation's files can set. */
|
|
39
|
-
const FIELDS = ["html", "frames", "scenarios", ...BLOCK_FIELDS];
|
|
53
|
+
/** Every field an animation's or clip's files can set. */
|
|
54
|
+
const FIELDS = ["html", "frames", "markers", "scenarios", ...BLOCK_FIELDS];
|
|
55
|
+
|
|
56
|
+
/** The block types that check out as files. */
|
|
57
|
+
type DraftType = "animation" | "clip";
|
|
40
58
|
|
|
41
59
|
/** What checkout.json records: the block the folder came from, as it was. */
|
|
42
60
|
const checkoutSchema = z.strictObject({
|
|
@@ -57,10 +75,12 @@ const handle = /^[\w-]{1,128}$/;
|
|
|
57
75
|
|
|
58
76
|
/** The animation references, read-only beside the drafts so they always match
|
|
59
77
|
* the Whiteboard that wrote them: whiteboard.css, the style kit every
|
|
60
|
-
* animation loads,
|
|
78
|
+
* animation loads, whiteboard.d.ts, the API its scripts call, and the
|
|
79
|
+
* reference animations an agent copies from. */
|
|
61
80
|
export interface StageReference {
|
|
62
81
|
styles: string;
|
|
63
82
|
api: string;
|
|
83
|
+
references: AnimationReference[];
|
|
64
84
|
}
|
|
65
85
|
|
|
66
86
|
async function writeReadOnly(file: string, content: string): Promise<void> {
|
|
@@ -69,27 +89,83 @@ async function writeReadOnly(file: string, content: string): Promise<void> {
|
|
|
69
89
|
await writeFile(file, content, { mode: 0o444 });
|
|
70
90
|
}
|
|
71
91
|
|
|
72
|
-
|
|
92
|
+
/** Copies `source` to `target` with every file read-only, replacing what was there. */
|
|
93
|
+
async function copyReadOnly(source: string, target: string): Promise<void> {
|
|
94
|
+
await rm(target, { recursive: true, force: true });
|
|
95
|
+
await cp(source, target, { recursive: true });
|
|
96
|
+
|
|
97
|
+
for (const entry of await readdir(target, {
|
|
98
|
+
recursive: true,
|
|
99
|
+
withFileTypes: true,
|
|
100
|
+
}))
|
|
101
|
+
if (entry.isFile())
|
|
102
|
+
await chmod(path.join(entry.parentPath, entry.name), 0o444);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function writeStageReference(
|
|
106
|
+
root = findReviewPackageRoot(import.meta.url),
|
|
107
|
+
): Promise<StageReference> {
|
|
73
108
|
const directory = path.join(devReviewHome(), "animation-drafts");
|
|
74
109
|
const styles = path.join(directory, "whiteboard.css");
|
|
75
110
|
const api = path.join(directory, "whiteboard.d.ts");
|
|
111
|
+
const copies = path.join(directory, "references");
|
|
112
|
+
const sources = await readAnimationReferences(root, { strict: false });
|
|
76
113
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
114
|
+
await mkdir(copies, { recursive: true });
|
|
115
|
+
await writeReadOnly(styles, stageStyleReference());
|
|
116
|
+
await writeReadOnly(
|
|
117
|
+
api,
|
|
118
|
+
await readFile(path.join(root, "instructions", "whiteboard.d.ts"), "utf8"),
|
|
81
119
|
);
|
|
82
120
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
121
|
+
const names = new Set(sources.map(({ name }) => name));
|
|
122
|
+
|
|
123
|
+
for (const stale of await readdir(copies))
|
|
124
|
+
if (!names.has(stale))
|
|
125
|
+
await rm(path.join(copies, stale), { recursive: true, force: true });
|
|
126
|
+
|
|
127
|
+
const references = await Promise.all(
|
|
128
|
+
sources.map(async (reference) => {
|
|
129
|
+
const copy = path.join(copies, reference.name);
|
|
86
130
|
|
|
87
|
-
|
|
131
|
+
await copyReadOnly(reference.directory, copy);
|
|
132
|
+
|
|
133
|
+
return { ...reference, directory: copy };
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return { styles, api, references };
|
|
88
138
|
}
|
|
89
139
|
|
|
90
140
|
/** The line that sends an agent to the references before it writes an animation. */
|
|
91
|
-
export const stageReferenceHint = ({
|
|
92
|
-
|
|
141
|
+
export const stageReferenceHint = ({
|
|
142
|
+
styles,
|
|
143
|
+
api,
|
|
144
|
+
references,
|
|
145
|
+
}: StageReference) =>
|
|
146
|
+
`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.`;
|
|
147
|
+
|
|
148
|
+
/** Lists the reference animations where the agent can read them. */
|
|
149
|
+
function referencesSection({ references }: StageReference): string[] {
|
|
150
|
+
if (!references.length) return [];
|
|
151
|
+
|
|
152
|
+
return [
|
|
153
|
+
[
|
|
154
|
+
"## References",
|
|
155
|
+
"",
|
|
156
|
+
"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.",
|
|
157
|
+
"",
|
|
158
|
+
...references.map(
|
|
159
|
+
({ name, summary, directory }) =>
|
|
160
|
+
`- \`${name}\`: ${summary} ${directory}`,
|
|
161
|
+
),
|
|
162
|
+
].join("\n"),
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** What follows the animation instructions: the hint, then the references. */
|
|
167
|
+
export const stageReferenceText = (reference: StageReference) =>
|
|
168
|
+
[stageReferenceHint(reference), ...referencesSection(reference)].join("\n\n");
|
|
93
169
|
|
|
94
170
|
export function draftDirectory(
|
|
95
171
|
sessionId: string,
|
|
@@ -110,9 +186,12 @@ const fingerprint = (block: JsonValue) =>
|
|
|
110
186
|
const json = (value: JsonValue) => `${JSON.stringify(value, null, 2)}\n`;
|
|
111
187
|
|
|
112
188
|
function animationBlock(block: JsonValue, targetId: string): JsonObject {
|
|
113
|
-
if (
|
|
189
|
+
if (
|
|
190
|
+
!isJsonObject(block) ||
|
|
191
|
+
(block.type !== "animation" && block.type !== "clip")
|
|
192
|
+
)
|
|
114
193
|
throw new ReviewInputError(
|
|
115
|
-
`${targetId} is ${isJsonObject(block) ? `a ${String(block.type)}` : "not a component"}; only animations check out as files.`,
|
|
194
|
+
`${targetId} is ${isJsonObject(block) ? `a ${String(block.type)}` : "not a component"}; only animations and clips check out as files.`,
|
|
116
195
|
);
|
|
117
196
|
|
|
118
197
|
return block;
|
|
@@ -143,7 +222,9 @@ export async function checkoutAnimation(
|
|
|
143
222
|
};
|
|
144
223
|
|
|
145
224
|
await put("html", String(block.html));
|
|
146
|
-
|
|
225
|
+
|
|
226
|
+
if (block.type === "clip") await put("markers", json(block.markers ?? []));
|
|
227
|
+
else await put("frames", json(block.frames ?? []));
|
|
147
228
|
|
|
148
229
|
if (block.scenarios !== undefined)
|
|
149
230
|
await put("scenarios", json(block.scenarios));
|
|
@@ -167,7 +248,8 @@ export async function checkoutAnimation(
|
|
|
167
248
|
return {
|
|
168
249
|
directory,
|
|
169
250
|
files,
|
|
170
|
-
|
|
251
|
+
styles: reference.styles,
|
|
252
|
+
api: reference.api,
|
|
171
253
|
next: `Edit these files with your file tools, then publish the folder: session_edit {sessionId, edit: {type: "update", targetId: "${targetId}", fromFiles: "${directory}"}}. ${stageReferenceHint(reference)}`,
|
|
172
254
|
};
|
|
173
255
|
}
|
|
@@ -184,7 +266,7 @@ async function readText(directory: string, name: string, required: boolean) {
|
|
|
184
266
|
|
|
185
267
|
if (required)
|
|
186
268
|
throw new ReviewInputError(
|
|
187
|
-
`${path.join(directory, name)} does not exist. Check the animation out with session_checkout, or write the file.`,
|
|
269
|
+
`${path.join(directory, name)} does not exist. Check the animation or clip out with session_checkout, or write the file.`,
|
|
188
270
|
);
|
|
189
271
|
|
|
190
272
|
return undefined;
|
|
@@ -205,16 +287,30 @@ async function readJson(directory: string, name: string, required: boolean) {
|
|
|
205
287
|
}
|
|
206
288
|
}
|
|
207
289
|
|
|
208
|
-
/** The animation a folder holds, as a block's fields
|
|
209
|
-
|
|
210
|
-
|
|
290
|
+
/** The animation or clip a folder holds, as a block's fields with its type,
|
|
291
|
+
* checked as a publish would be. frames.json makes it an animation and
|
|
292
|
+
* markers.json a clip. */
|
|
293
|
+
export async function readAnimationFolder(
|
|
294
|
+
directory: string,
|
|
295
|
+
): Promise<JsonObject & { type: DraftType }> {
|
|
296
|
+
const [html, frames, markers, scenarios, block] = await Promise.all([
|
|
211
297
|
readText(directory, FILES.html, true),
|
|
212
|
-
readJson(directory, FILES.frames,
|
|
298
|
+
readJson(directory, FILES.frames, false),
|
|
299
|
+
readJson(directory, FILES.markers, false),
|
|
213
300
|
readJson(directory, FILES.scenarios, false),
|
|
214
301
|
readJson(directory, FILES.block, true),
|
|
215
|
-
readJson(directory, FILES.checkout, false),
|
|
216
302
|
]);
|
|
217
303
|
|
|
304
|
+
if (frames !== undefined && markers !== undefined)
|
|
305
|
+
throw new ReviewInputError(
|
|
306
|
+
`${directory} holds both ${FILES.frames} and ${FILES.markers}. Keep ${FILES.frames} for an animation or ${FILES.markers} for a clip, not both.`,
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
if (frames === undefined && markers === undefined)
|
|
310
|
+
throw new ReviewInputError(
|
|
311
|
+
`${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.`,
|
|
312
|
+
);
|
|
313
|
+
|
|
218
314
|
if (!isJsonObject(block))
|
|
219
315
|
throw new ReviewInputError(`${FILES.block} must hold a JSON object.`);
|
|
220
316
|
|
|
@@ -222,15 +318,28 @@ async function readDraft(directory: string) {
|
|
|
222
318
|
|
|
223
319
|
if (stray.length)
|
|
224
320
|
throw new ReviewInputError(
|
|
225
|
-
`${FILES.block} holds only ${BLOCK_FIELDS.join(", ")}; move ${stray.join(", ")} out (html is ${FILES.html}, frames ${FILES.frames}, scenarios ${FILES.scenarios}).`,
|
|
321
|
+
`${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}).`,
|
|
226
322
|
);
|
|
227
323
|
|
|
228
|
-
const fields: JsonObject
|
|
324
|
+
const fields: JsonObject & { type: DraftType } =
|
|
325
|
+
markers === undefined
|
|
326
|
+
? { ...block, type: "animation", html: html!, frames: frames! }
|
|
327
|
+
: { ...block, type: "clip", html: html!, markers };
|
|
229
328
|
|
|
230
329
|
if (scenarios !== undefined) fields.scenarios = scenarios;
|
|
231
330
|
|
|
232
331
|
validate(fields);
|
|
233
332
|
|
|
333
|
+
return fields;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** The animation a folder holds, as a block's fields, and where it came from. */
|
|
337
|
+
async function readDraft(directory: string) {
|
|
338
|
+
const [fields, checkout] = await Promise.all([
|
|
339
|
+
readAnimationFolder(directory),
|
|
340
|
+
readJson(directory, FILES.checkout, false),
|
|
341
|
+
]);
|
|
342
|
+
|
|
234
343
|
const parsed = checkoutSchema.safeParse(checkout);
|
|
235
344
|
|
|
236
345
|
return { fields, checkout: parsed.success ? parsed.data : undefined };
|
|
@@ -246,7 +355,7 @@ function located(issuePath: PropertyKey[]): string {
|
|
|
246
355
|
|
|
247
356
|
if (field === "html") return FILES.html;
|
|
248
357
|
|
|
249
|
-
if (field === "frames" || field === "scenarios")
|
|
358
|
+
if (field === "frames" || field === "markers" || field === "scenarios")
|
|
250
359
|
return `${FILES[field]} ${at.replace(/^\./, "") || "(top level)"}`;
|
|
251
360
|
|
|
252
361
|
return `${FILES.block} ${field ?? ""}${at}`;
|
|
@@ -254,13 +363,9 @@ function located(issuePath: PropertyKey[]): string {
|
|
|
254
363
|
|
|
255
364
|
/** Checks the files before anything is sent, so an issue names its file. The
|
|
256
365
|
* host checks pins: agents write them without the repository it fills in. */
|
|
257
|
-
function validate(fields: JsonObject) {
|
|
366
|
+
function validate(fields: JsonObject & { type: DraftType }) {
|
|
258
367
|
const { pins: _pins, ...rest } = fields;
|
|
259
|
-
|
|
260
|
-
const parsed = blocks.animation.schema.safeParse({
|
|
261
|
-
...rest,
|
|
262
|
-
type: "animation",
|
|
263
|
-
});
|
|
368
|
+
const parsed = blocks[fields.type].schema.safeParse(rest);
|
|
264
369
|
|
|
265
370
|
if (!parsed.success)
|
|
266
371
|
throw new ReviewInputError(
|
|
@@ -274,8 +379,8 @@ function validate(fields: JsonObject) {
|
|
|
274
379
|
|
|
275
380
|
/**
|
|
276
381
|
* Turns an edit that names a draft folder into the edit the host takes: the
|
|
277
|
-
* folder's animation as an insert's or replace's content, or as an
|
|
278
|
-
* changed fields. An update from a checkout is refused once the block has
|
|
382
|
+
* folder's animation or clip as an insert's or replace's content, or as an
|
|
383
|
+
* update's changed fields. An update from a checkout is refused once the block has
|
|
279
384
|
* changed since. Returns `unchanged` when the files match the block.
|
|
280
385
|
*/
|
|
281
386
|
export async function resolveDraftEdit(
|
|
@@ -295,10 +400,7 @@ export async function resolveDraftEdit(
|
|
|
295
400
|
const { fields, checkout } = await readDraft(directory);
|
|
296
401
|
|
|
297
402
|
if (rest.type === "insert")
|
|
298
|
-
return {
|
|
299
|
-
directory,
|
|
300
|
-
edit: { ...rest, content: { type: "animation", ...fields } },
|
|
301
|
-
};
|
|
403
|
+
return { directory, edit: { ...rest, content: fields } };
|
|
302
404
|
|
|
303
405
|
const targetId = String(rest.targetId);
|
|
304
406
|
const current = animationBlock(await read(sessionId, targetId), targetId);
|
|
@@ -315,16 +417,18 @@ export async function resolveDraftEdit(
|
|
|
315
417
|
);
|
|
316
418
|
|
|
317
419
|
if (rest.type === "replace")
|
|
318
|
-
return {
|
|
319
|
-
directory,
|
|
320
|
-
edit: { ...rest, content: { type: "animation", ...fields } },
|
|
321
|
-
};
|
|
420
|
+
return { directory, edit: { ...rest, content: fields } };
|
|
322
421
|
|
|
323
422
|
if (rest.type !== "update")
|
|
324
423
|
throw new ReviewInputError(
|
|
325
424
|
"fromFiles publishes an insert, replace or update.",
|
|
326
425
|
);
|
|
327
426
|
|
|
427
|
+
if (current.type !== fields.type)
|
|
428
|
+
throw new ReviewInputError(
|
|
429
|
+
`${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.`,
|
|
430
|
+
);
|
|
431
|
+
|
|
328
432
|
const changes: JsonObject = {};
|
|
329
433
|
|
|
330
434
|
for (const field of FIELDS)
|