@dev.fast/whiteboard 0.5.0 → 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 +6 -6
- 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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// The reference animations shipped in instructions/animations: worked examples
|
|
2
|
+
// an agent copies as a starting point. Each folder is an animation draft plus a
|
|
3
|
+
// README, read by the same code that publishes a draft.
|
|
4
|
+
|
|
5
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
import { ReviewInputError } from "@dev.fast/whiteboard-document";
|
|
9
|
+
import { isMissingFileError } from "@review/fs-utils.js";
|
|
10
|
+
import { findReviewPackageRoot } from "@review/package-paths.js";
|
|
11
|
+
|
|
12
|
+
import { readAnimationFolder } from "./animation-drafts.js";
|
|
13
|
+
|
|
14
|
+
export type AnimationReference = {
|
|
15
|
+
name: string;
|
|
16
|
+
directory: string;
|
|
17
|
+
title: string;
|
|
18
|
+
summary: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** The README's H1 and the paragraph right after it. */
|
|
22
|
+
function readmeHeading(file: string, text: string) {
|
|
23
|
+
const lines = text.split(/\r?\n/);
|
|
24
|
+
const start = lines.findIndex((line) => line.trim());
|
|
25
|
+
const title = lines[start]?.match(/^# (.+)$/)?.[1]?.trim();
|
|
26
|
+
const rest = lines.slice(start + 1);
|
|
27
|
+
const from = rest.findIndex((line) => line.trim());
|
|
28
|
+
const paragraph = [];
|
|
29
|
+
|
|
30
|
+
for (const line of from < 0 ? [] : rest.slice(from)) {
|
|
31
|
+
if (!line.trim() || line.startsWith("#")) break;
|
|
32
|
+
paragraph.push(line.trim());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const summary = paragraph.join(" ");
|
|
36
|
+
|
|
37
|
+
if (!title || !summary)
|
|
38
|
+
throw new ReviewInputError(
|
|
39
|
+
`${file} must open with "# Title" followed by a one-sentence summary paragraph.`,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return { title, summary };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Every reference animation, sorted by name, each checked as a publish would
|
|
46
|
+
* be. Strict (the default) throws on a broken reference, for the tests that
|
|
47
|
+
* keep them valid; otherwise a broken one is left out, so an agent's checkout
|
|
48
|
+
* and instructions never fail on a reference. */
|
|
49
|
+
export async function readAnimationReferences(
|
|
50
|
+
root = findReviewPackageRoot(import.meta.url),
|
|
51
|
+
{ strict = true } = {},
|
|
52
|
+
): Promise<AnimationReference[]> {
|
|
53
|
+
const parent = path.join(root, "instructions", "animations");
|
|
54
|
+
let entries;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
entries = await readdir(parent, { withFileTypes: true });
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (isMissingFileError(error)) return [];
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const names = entries
|
|
64
|
+
.filter((entry) => entry.isDirectory())
|
|
65
|
+
.map((entry) => entry.name)
|
|
66
|
+
.sort();
|
|
67
|
+
|
|
68
|
+
const references = await Promise.all(
|
|
69
|
+
names.map(async (name): Promise<AnimationReference | undefined> => {
|
|
70
|
+
const directory = path.join(parent, name);
|
|
71
|
+
const readme = path.join(directory, "README.md");
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await readAnimationFolder(directory);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
name,
|
|
78
|
+
directory,
|
|
79
|
+
...readmeHeading(readme, await readFile(readme, "utf8")),
|
|
80
|
+
};
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (!strict) return undefined;
|
|
83
|
+
|
|
84
|
+
throw new ReviewInputError(
|
|
85
|
+
`Reference animation ${name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return references.filter((reference) => reference !== undefined);
|
|
92
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// An agent's look at an animation it wrote: one frame of it (or a clip at
|
|
2
|
+
// one marker), shown by the attached Desktop as the reader sees it and
|
|
3
|
+
// captured as a PNG. The server checks the stop against the document; the
|
|
4
|
+
// Desktop shows and captures it.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type Element,
|
|
8
|
+
ReviewInputError,
|
|
9
|
+
elements,
|
|
10
|
+
} from "@dev.fast/whiteboard-document";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
import type { Snapshot } from "./store.js";
|
|
14
|
+
|
|
15
|
+
export const renderQuerySchema = z.object({
|
|
16
|
+
targetId: z.string().min(1),
|
|
17
|
+
/** A frame key, or a clip's marker key; omitted, the first stop. */
|
|
18
|
+
frame: z.string().min(1).optional(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/** What the Desktop answers: the stop it showed, and the figure's PNG. */
|
|
22
|
+
export const renderedFrameSchema = z.object({
|
|
23
|
+
frame: z.object({
|
|
24
|
+
key: z.string(),
|
|
25
|
+
title: z.string(),
|
|
26
|
+
position: z.number().int().nonnegative(),
|
|
27
|
+
count: z.number().int().positive(),
|
|
28
|
+
}),
|
|
29
|
+
scenario: z.string().optional(),
|
|
30
|
+
/** The figure ran past the window, so the capture cuts it. */
|
|
31
|
+
clipped: z.boolean(),
|
|
32
|
+
/** The error the stage reported, if its script threw. */
|
|
33
|
+
error: z.string().nullable(),
|
|
34
|
+
/** The capture, base64. */
|
|
35
|
+
png: z.string().min(1),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type RenderedFrame = z.infer<typeof renderedFrameSchema>;
|
|
39
|
+
|
|
40
|
+
export interface RenderRequest {
|
|
41
|
+
reviewId: string;
|
|
42
|
+
targetId: string;
|
|
43
|
+
frame: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The host's way to a Desktop that shows the frame and captures it. */
|
|
47
|
+
export type RenderAnimation = (
|
|
48
|
+
request: RenderRequest,
|
|
49
|
+
) => Promise<RenderedFrame>;
|
|
50
|
+
|
|
51
|
+
/** The stop a render shows: a frame of an animation, or a marker of a clip. */
|
|
52
|
+
export interface RenderTarget {
|
|
53
|
+
type: "animation" | "clip";
|
|
54
|
+
key: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type StageBlock = Extract<Element, { type: "animation" | "clip" }>;
|
|
58
|
+
|
|
59
|
+
const isStageBlock = (element: Element): element is StageBlock =>
|
|
60
|
+
element.type === "animation" || element.type === "clip";
|
|
61
|
+
|
|
62
|
+
/** The stop to render: `frame` checked against the block's frames or markers, or
|
|
63
|
+
* the first stop of the first scenario when omitted. */
|
|
64
|
+
export function renderTarget(
|
|
65
|
+
snapshot: Snapshot,
|
|
66
|
+
targetId: string,
|
|
67
|
+
frame: string | undefined,
|
|
68
|
+
): RenderTarget {
|
|
69
|
+
const target = elements(snapshot.document).find(
|
|
70
|
+
(element) => element.id === targetId,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (!target)
|
|
74
|
+
throw new ReviewInputError("Target not found in this version.", 404);
|
|
75
|
+
|
|
76
|
+
if (!isStageBlock(target))
|
|
77
|
+
throw new ReviewInputError(
|
|
78
|
+
`${targetId} is a ${target.type}; only an animation or a clip renders a frame.`,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const noun = target.type === "clip" ? "marker" : "frame";
|
|
82
|
+
|
|
83
|
+
const keys =
|
|
84
|
+
target.type === "clip"
|
|
85
|
+
? target.markers.map((marker) => marker.key)
|
|
86
|
+
: target.frames.map((item) => item.key);
|
|
87
|
+
|
|
88
|
+
if (frame === undefined) {
|
|
89
|
+
const first =
|
|
90
|
+
target.type === "animation"
|
|
91
|
+
? (target.scenarios?.[0]?.frames[0] ?? keys[0])
|
|
92
|
+
: keys[0];
|
|
93
|
+
|
|
94
|
+
if (first === undefined)
|
|
95
|
+
throw new ReviewInputError(`${targetId} has no ${noun}s to render.`);
|
|
96
|
+
|
|
97
|
+
return { type: target.type, key: first };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!keys.includes(frame))
|
|
101
|
+
throw new ReviewInputError(
|
|
102
|
+
`${frame} is not a ${noun} of ${targetId}. Its ${noun}s: ${keys.join(", ")}.`,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return { type: target.type, key: frame };
|
|
106
|
+
}
|
|
@@ -52,7 +52,7 @@ export function authoringTools(
|
|
|
52
52
|
"While a reader may be watching, write small and often: one paragraph per edit. Insert a new diagram whole. Change one already drawn a unit at a time (a flow_node, flow_edge or step, with parentId naming the diagram), and link each added flow_node to a node already drawn.",
|
|
53
53
|
...(animationsEnabled
|
|
54
54
|
? [
|
|
55
|
-
"An animation is edited as files: review_checkout writes it to a folder, you edit the files with your own file tools, and an edit with fromFiles naming that folder publishes it in place of content or changes. Publish the same folder again after each round of edits.",
|
|
55
|
+
"An animation or clip is edited as files: review_checkout writes it to a folder, you edit the files with your own file tools, and an edit with fromFiles naming that folder publishes it in place of content or changes. Publish the same folder again after each round of edits.",
|
|
56
56
|
]
|
|
57
57
|
: []),
|
|
58
58
|
].join("\n"),
|
|
@@ -177,11 +177,18 @@ export function authoringTools(
|
|
|
177
177
|
? [
|
|
178
178
|
tool(
|
|
179
179
|
"checkout",
|
|
180
|
-
"Write an animation to a folder of files to edit with your own file tools: animation.html (its html), frames.json (
|
|
180
|
+
"Write an animation or clip to a folder of files to edit with your own file tools: animation.html (its html), frames.json (an animation's frames, each with its data) or markers.json (a clip's markers), scenarios.json and block.json (title, code, height, pins). Then publish the folder with review_edit, giving fromFiles instead of content or changes. dir defaults to a folder Whiteboard manages; checking out again overwrites it. A publish is refused if the block changed after the checkout.",
|
|
181
181
|
z.strictObject({ ...review, targetId: id, dir: id.optional() }),
|
|
182
182
|
"GET",
|
|
183
183
|
"/:reviewId/inspect",
|
|
184
184
|
),
|
|
185
|
+
tool(
|
|
186
|
+
"render",
|
|
187
|
+
"See an animation or clip as the reader does: Desktop shows one of its frames (frame: a frame key, or a clip's marker key; omitted, the first) on the open whiteboard and returns it as a PNG image, with the error its script threw, if any. The PNG is also written to the path the result names. After publishing, render each frame you changed and look at it, instead of taking screenshots of the screen. The whiteboard must be open in Desktop: review_create opens it, and review_open opens an existing one.",
|
|
188
|
+
z.strictObject({ ...review, targetId: id, frame: id.optional() }),
|
|
189
|
+
"GET",
|
|
190
|
+
"/:reviewId/render",
|
|
191
|
+
),
|
|
185
192
|
]
|
|
186
193
|
: []),
|
|
187
194
|
tool("list", "List saved reviews.", z.strictObject({}), "GET", ""),
|
|
@@ -244,7 +251,7 @@ function withDraftFiles(edit: ReturnType<typeof publishedEdits>) {
|
|
|
244
251
|
.string()
|
|
245
252
|
.min(1)
|
|
246
253
|
.describe(
|
|
247
|
-
"An animation's draft folder, from review_checkout or written yourself: publishes its files as the content (insert, replace) or changes (update).",
|
|
254
|
+
"An animation's or clip's draft folder, from review_checkout or written yourself: publishes its files as the content (insert, replace) or changes (update). frames.json makes it an animation, markers.json a clip.",
|
|
248
255
|
);
|
|
249
256
|
|
|
250
257
|
const [insert, replace, update, move, remove] = edit.options;
|
|
@@ -201,21 +201,36 @@ export function documentText(
|
|
|
201
201
|
);
|
|
202
202
|
break;
|
|
203
203
|
case "animation":
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
case "clip":
|
|
205
|
+
if (element.type === "animation") {
|
|
206
|
+
for (const [index, frame] of element.frames.entries()) {
|
|
207
|
+
detail(
|
|
208
|
+
`Frame ${index + 1} [${frame.key}]: ${frame.title}${frame.caption ? ` — ${frame.caption}` : ""}`,
|
|
209
|
+
);
|
|
208
210
|
|
|
209
|
-
|
|
211
|
+
if (frame.code) write(depth + 2, `Code: ${frame.code.join(", ")}`);
|
|
210
212
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
213
|
+
if (detailed && frame.data !== undefined)
|
|
214
|
+
write(depth + 2, `Data: ${JSON.stringify(frame.data)}`);
|
|
215
|
+
}
|
|
214
216
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
for (const scenario of element.scenarios ?? [])
|
|
218
|
+
detail(
|
|
219
|
+
`Scenario [${scenario.key}] ${scenario.label}: ${scenario.frames.join(" → ")}`,
|
|
220
|
+
);
|
|
221
|
+
} else {
|
|
222
|
+
for (const [index, marker] of element.markers.entries()) {
|
|
223
|
+
detail(
|
|
224
|
+
`Marker ${index + 1} [${marker.key}]: ${marker.title}${marker.caption ? ` — ${marker.caption}` : ""}`,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if (marker.code)
|
|
228
|
+
write(depth + 2, `Code: ${marker.code.join(", ")}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const scenario of element.scenarios ?? [])
|
|
232
|
+
detail(`Scenario [${scenario.key}] ${scenario.label}`);
|
|
233
|
+
}
|
|
219
234
|
|
|
220
235
|
if (runs) {
|
|
221
236
|
const run =
|
package/src/review-api/http.ts
CHANGED
|
@@ -57,6 +57,7 @@ import { resolveReviewStackLayers } from "@review/review-stack.js";
|
|
|
57
57
|
import { readBoundedRequestJson } from "@review/server/hono-http.js";
|
|
58
58
|
import { HttpJsonError } from "@review/server/http-json.js";
|
|
59
59
|
import {
|
|
60
|
+
type RemoteShare,
|
|
60
61
|
type SharingHostEvents,
|
|
61
62
|
mountSharingHost,
|
|
62
63
|
} from "@review/sharing/host.js";
|
|
@@ -67,6 +68,11 @@ import { type Context, Hono, type MiddlewareHandler } from "hono";
|
|
|
67
68
|
import { z } from "zod";
|
|
68
69
|
|
|
69
70
|
import { anchorQuotes } from "./anchor-quotes.js";
|
|
71
|
+
import {
|
|
72
|
+
type RenderAnimation,
|
|
73
|
+
renderQuerySchema,
|
|
74
|
+
renderTarget,
|
|
75
|
+
} from "./animation-render.js";
|
|
70
76
|
import type { AskRecord } from "./ask-history.js";
|
|
71
77
|
import { authoringTools } from "./authoring-tools.js";
|
|
72
78
|
import { documentText } from "./document-text.js";
|
|
@@ -172,7 +178,7 @@ export interface ReviewApiHooks {
|
|
|
172
178
|
via: ReviewRequestVia;
|
|
173
179
|
agentKind?: ReviewSessionAgent;
|
|
174
180
|
}) => void;
|
|
175
|
-
sharing?: SharingHostEvents;
|
|
181
|
+
sharing?: SharingHostEvents & { remoteShare?: RemoteShare };
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
/** A person's save carries the whole board, which edits one at a time can
|
|
@@ -207,6 +213,8 @@ export function createReviewApi(
|
|
|
207
213
|
/** Desktop's Ask: local agents answering questions about a selection. */
|
|
208
214
|
ask?: AskHost,
|
|
209
215
|
animationsEnabled: () => boolean = () => false,
|
|
216
|
+
/** Shows a frame of an animation in the attached Desktop and captures it. */
|
|
217
|
+
render?: RenderAnimation,
|
|
210
218
|
) {
|
|
211
219
|
const app = new Hono();
|
|
212
220
|
app.onError((error, context) => {
|
|
@@ -2085,6 +2093,32 @@ export function createReviewApi(
|
|
|
2085
2093
|
},
|
|
2086
2094
|
);
|
|
2087
2095
|
});
|
|
2096
|
+
// One frame of an animation (or a clip at a marker) as the reader sees it:
|
|
2097
|
+
// the attached Desktop shows it on the open board and captures it.
|
|
2098
|
+
app.get("/:id/render", async (context) => {
|
|
2099
|
+
const query = renderQuerySchema.parse(context.req.query());
|
|
2100
|
+
const id = context.req.param("id");
|
|
2101
|
+
const target = renderTarget(readReview(id), query.targetId, query.frame);
|
|
2102
|
+
|
|
2103
|
+
if (!render)
|
|
2104
|
+
throw new ReviewInputError(
|
|
2105
|
+
"Rendering a frame needs Whiteboard Desktop, which this server cannot reach.",
|
|
2106
|
+
409,
|
|
2107
|
+
);
|
|
2108
|
+
|
|
2109
|
+
const rendered = await render({
|
|
2110
|
+
reviewId: id,
|
|
2111
|
+
targetId: query.targetId,
|
|
2112
|
+
frame: target.key,
|
|
2113
|
+
});
|
|
2114
|
+
|
|
2115
|
+
return context.json({
|
|
2116
|
+
reviewId: id,
|
|
2117
|
+
targetId: query.targetId,
|
|
2118
|
+
type: target.type,
|
|
2119
|
+
...rendered,
|
|
2120
|
+
});
|
|
2121
|
+
});
|
|
2088
2122
|
app.get("/:id", async (context) => {
|
|
2089
2123
|
const query = readQuerySchemas.get.parse(context.req.query());
|
|
2090
2124
|
|
|
@@ -2170,12 +2204,15 @@ export function createReviewApi(
|
|
|
2170
2204
|
)
|
|
2171
2205
|
: undefined;
|
|
2172
2206
|
|
|
2207
|
+
const animated = (type: string | undefined) =>
|
|
2208
|
+
type === "animation" || type === "clip";
|
|
2209
|
+
|
|
2173
2210
|
if (
|
|
2174
|
-
incoming.some((element) => element.type
|
|
2175
|
-
target?.type
|
|
2211
|
+
incoming.some((element) => animated(element.type)) ||
|
|
2212
|
+
animated(target?.type)
|
|
2176
2213
|
)
|
|
2177
2214
|
throw new ReviewInputError(
|
|
2178
|
-
"Animation authoring is disabled. Enable Animations in Settings under Experimental Features to edit animations. Existing
|
|
2215
|
+
"Animation authoring is disabled. Enable Animations in Settings under Experimental Features to edit animations and clips. Existing ones can still be viewed.",
|
|
2179
2216
|
409,
|
|
2180
2217
|
);
|
|
2181
2218
|
}
|
|
@@ -2196,7 +2233,7 @@ export function createReviewApi(
|
|
|
2196
2233
|
...(threadId && { threadId }),
|
|
2197
2234
|
};
|
|
2198
2235
|
|
|
2199
|
-
// A
|
|
2236
|
+
// A link's kept commits go when its last copy is deleted for good.
|
|
2200
2237
|
const copy =
|
|
2201
2238
|
input.operation.type === "delete" && store.has(input.operation.reviewId)
|
|
2202
2239
|
? store.read(input.operation.reviewId)
|
|
@@ -2206,7 +2243,16 @@ export function createReviewApi(
|
|
|
2206
2243
|
const key = copy?.shared?.key;
|
|
2207
2244
|
const kept = copy && boardRepositoryId(copy);
|
|
2208
2245
|
|
|
2209
|
-
if (
|
|
2246
|
+
if (
|
|
2247
|
+
result.deleted &&
|
|
2248
|
+
key &&
|
|
2249
|
+
kept &&
|
|
2250
|
+
!store
|
|
2251
|
+
.copiesOf(key)
|
|
2252
|
+
.some(
|
|
2253
|
+
({ reviewId }) => boardRepositoryId(store.read(reviewId)) === kept,
|
|
2254
|
+
)
|
|
2255
|
+
)
|
|
2210
2256
|
await Promise.resolve()
|
|
2211
2257
|
.then(() => forgetSharedCommits(store.repositoryPath(kept), key))
|
|
2212
2258
|
.catch((error) =>
|
|
@@ -30,7 +30,7 @@ export interface InstructionContext {
|
|
|
30
30
|
/** Keep disabled experimental components out of agent guidance. */
|
|
31
31
|
export const desktopBlockTypes = (animationsEnabled: boolean) =>
|
|
32
32
|
writableBlockTypes.filter(
|
|
33
|
-
(type) => animationsEnabled || type !== "animation",
|
|
33
|
+
(type) => animationsEnabled || (type !== "animation" && type !== "clip"),
|
|
34
34
|
);
|
|
35
35
|
|
|
36
36
|
const COMPONENT_EXAMPLE =
|
package/src/review-api/mcp.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { authoringTools } from "./authoring-tools.js";
|
|
|
17
17
|
import type { ReviewApiClient } from "./client.js";
|
|
18
18
|
import { ReviewApiError } from "./client.js";
|
|
19
19
|
import { mcpClientAgent } from "./mcp-client-agent.js";
|
|
20
|
-
import { callPublicTool, publicTool } from "./public-tools.js";
|
|
20
|
+
import { callPublicTool, publicTool, renderedImage } from "./public-tools.js";
|
|
21
21
|
import { RECOVERY } from "./recovery.js";
|
|
22
22
|
import { REVIEW_STATUS_TOOL } from "./status-tool.js";
|
|
23
23
|
import { type ToolFailure, toolFailure } from "./tool-failure.js";
|
|
@@ -191,11 +191,14 @@ export async function serveReviewMcp(
|
|
|
191
191
|
`Unknown Whiteboard tool: ${request.params.name}`,
|
|
192
192
|
);
|
|
193
193
|
|
|
194
|
-
const result =
|
|
195
|
-
client,
|
|
194
|
+
const { image, result } = renderedImage(
|
|
196
195
|
tool,
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
await callPublicTool(
|
|
197
|
+
client,
|
|
198
|
+
tool,
|
|
199
|
+
request.params.arguments ?? {},
|
|
200
|
+
ctx.mcpReq.signal,
|
|
201
|
+
),
|
|
199
202
|
);
|
|
200
203
|
|
|
201
204
|
const text = toolResultText(tool, result);
|
|
@@ -203,6 +206,8 @@ export async function serveReviewMcp(
|
|
|
203
206
|
|
|
204
207
|
const response: CallToolResult = {
|
|
205
208
|
content: [
|
|
209
|
+
// The frame an agent rendered, ahead of what the host says about it.
|
|
210
|
+
...(image ? [{ type: "image" as const, ...image }] : []),
|
|
206
211
|
{
|
|
207
212
|
type: "text",
|
|
208
213
|
text:
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
1
4
|
import { type JsonValue, isJsonObject, isStringValue } from "@dev.fast/json";
|
|
2
5
|
import { boardTitle } from "@dev.fast/review-protocol";
|
|
6
|
+
import { devReviewHome } from "@dev.fast/trace-core";
|
|
3
7
|
import { ReviewInputError } from "@dev.fast/whiteboard-document";
|
|
4
8
|
|
|
5
9
|
import {
|
|
@@ -12,7 +16,7 @@ import {
|
|
|
12
16
|
checkoutAnimation,
|
|
13
17
|
recordPublish,
|
|
14
18
|
resolveDraftEdit,
|
|
15
|
-
|
|
19
|
+
stageReferenceText,
|
|
16
20
|
writeStageReference,
|
|
17
21
|
} from "./animation-drafts.js";
|
|
18
22
|
import type { ReviewApiClient } from "./client.js";
|
|
@@ -131,7 +135,7 @@ export async function callPublicTool(
|
|
|
131
135
|
|
|
132
136
|
if (tool.name === "session_get_instructions" && rest.topic === "animation") {
|
|
133
137
|
const instructions = await callAuthoringTool(client, tool, fields, signal);
|
|
134
|
-
const hint =
|
|
138
|
+
const hint = stageReferenceText(await writeStageReference());
|
|
135
139
|
|
|
136
140
|
if (instructions instanceof ToolText)
|
|
137
141
|
return new ToolText(`${instructions.text}\n\n${hint}`);
|
|
@@ -171,6 +175,13 @@ export async function callPublicTool(
|
|
|
171
175
|
if (result instanceof ToolText) return result;
|
|
172
176
|
const published = withoutRepositoryIds(publicResult(result));
|
|
173
177
|
|
|
178
|
+
if (
|
|
179
|
+
tool.name === "session_render" &&
|
|
180
|
+
isStringValue(sessionId) &&
|
|
181
|
+
isStringValue(rest.targetId)
|
|
182
|
+
)
|
|
183
|
+
return saveRenderedFrame(published, sessionId, rest.targetId);
|
|
184
|
+
|
|
174
185
|
if (
|
|
175
186
|
draft &&
|
|
176
187
|
isStringValue(sessionId) &&
|
|
@@ -189,6 +200,64 @@ export async function callPublicTool(
|
|
|
189
200
|
return published;
|
|
190
201
|
}
|
|
191
202
|
|
|
203
|
+
/** A tool result with its rendered frame, if any, taken out as an image. */
|
|
204
|
+
export interface RenderedImage {
|
|
205
|
+
image?: { data: string; mimeType: "image/png" };
|
|
206
|
+
result: JsonValue | ToolText;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** A rendered frame's PNG, for an agent that reads images from a tool
|
|
210
|
+
* result, and the rest of the result for its text. */
|
|
211
|
+
export function renderedImage(
|
|
212
|
+
tool: Pick<AuthoringTool, "name">,
|
|
213
|
+
result: JsonValue | ToolText,
|
|
214
|
+
): RenderedImage {
|
|
215
|
+
if (
|
|
216
|
+
tool.name !== "session_render" ||
|
|
217
|
+
result instanceof ToolText ||
|
|
218
|
+
!isJsonObject(result) ||
|
|
219
|
+
!isStringValue(result.png)
|
|
220
|
+
)
|
|
221
|
+
return { result };
|
|
222
|
+
|
|
223
|
+
const { png, ...rest } = result;
|
|
224
|
+
|
|
225
|
+
return { image: { data: png, mimeType: "image/png" }, result: rest };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Writes the frame's PNG where the agent can open it with its own tools,
|
|
229
|
+
* beside the animation drafts, and names the file in the result. A folder
|
|
230
|
+
* that cannot be written leaves the image in the result alone. */
|
|
231
|
+
async function saveRenderedFrame(
|
|
232
|
+
result: JsonValue,
|
|
233
|
+
sessionId: string,
|
|
234
|
+
targetId: string,
|
|
235
|
+
): Promise<JsonValue> {
|
|
236
|
+
if (!isJsonObject(result) || !isStringValue(result.png)) return result;
|
|
237
|
+
|
|
238
|
+
const key =
|
|
239
|
+
isJsonObject(result.frame) && isStringValue(result.frame.key)
|
|
240
|
+
? result.frame.key
|
|
241
|
+
: "frame";
|
|
242
|
+
|
|
243
|
+
const file = path.join(
|
|
244
|
+
devReviewHome(),
|
|
245
|
+
"animation-renders",
|
|
246
|
+
...[sessionId, targetId, `${key}.png`].map((part) =>
|
|
247
|
+
part.replace(/[^\w.-]/g, "_"),
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
253
|
+
await writeFile(file, Buffer.from(result.png, "base64"));
|
|
254
|
+
} catch {
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { ...result, file };
|
|
259
|
+
}
|
|
260
|
+
|
|
192
261
|
/** The read a draft compares and records against: session_get's JSON. */
|
|
193
262
|
const INSPECT: AuthoringTool = {
|
|
194
263
|
name: "review_get",
|