@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.
Files changed (117) hide show
  1. package/dist/{account-alias-Bw3PT3Qo.js → account-alias-BlYamXAG.js} +5 -5
  2. package/dist/{agent-cli-kHRrwO4n.js → agent-cli-D7fF5xmm.js} +9 -9
  3. package/dist/agent-cli-DLibAL4u.js +2 -0
  4. package/dist/{agent-client-CMacGbMp.js → agent-client-BCEbSPWU.js} +4 -4
  5. package/dist/{authoring-tools-CiLy46kh.js → authoring-tools-CkfxTVJm.js} +12 -8
  6. package/dist/{background-server-eZWXoVcI.js → background-server-BraP8pJ9.js} +4 -4
  7. package/dist/{background-server-BLNHbksB.js → background-server-CwToVN1g.js} +1 -1
  8. package/dist/build-info.json +1 -1
  9. package/dist/{cli-Bxmiqutp.js → cli-BpadBaGU.js} +1 -1
  10. package/dist/{cli-runner-DQPuu7kJ.js → cli-runner-BebYF50a.js} +22 -22
  11. package/dist/cli.js +2 -2
  12. package/dist/{fuzzy-match-DZjeOaPY.js → fuzzy-match-CxcsLQGx.js} +3 -3
  13. package/dist/{headless-host-Dl2KZ9IT.js → headless-host-FAlbekFD.js} +561 -415
  14. package/dist/{headless-host-B4r0s1ks.js → headless-host-cLipIufH.js} +1 -1
  15. package/dist/{import-YAIU6rB0.js → import-CZ1958L4.js} +234 -62
  16. package/dist/{local-data-C58vzLZR.js → local-data-DdFkxFK_.js} +41 -16
  17. package/dist/{mcp-COYWiGVn.js → mcp-Claiy9xE.js} +9 -6
  18. package/dist/{package-paths-Cg6fnaGp.js → package-paths-Dk7MOXxN.js} +1 -1
  19. package/dist/{profile--BYIjPkc.js → profile-Bsgzi9PA.js} +2 -2
  20. package/dist/profile-BxdzBxHW.js +2 -0
  21. package/dist/{remote-attach-C1prLjmj.js → remote-attach-LGTpwpUv.js} +4 -4
  22. package/dist/{remote-extensions-Bw029iIS.js → remote-extensions-BvV29_gk.js} +1 -1
  23. package/dist/{remote-extensions-MUUgbLJn.js → remote-extensions-L95HNwIH.js} +2 -2
  24. package/dist/{remote-language-server-z3rdHcLL.js → remote-language-server-BAznkpeu.js} +4 -4
  25. package/dist/{remote-uninstall-B3Slwor1.js → remote-uninstall-CLP79Cuk.js} +5 -5
  26. package/dist/{request-origin-B3_XI2VA.js → request-origin-D6B-zrL8.js} +27 -5
  27. package/dist/{review-app-launcher-BZEbDKuK.js → review-app-launcher-tTe8bTwg.js} +2 -2
  28. package/dist/{review-home-paths-LdGS4SpR.js → review-home-paths-BmZX2qiz.js} +1 -1
  29. package/dist/{review-telemetry-x8u-9EZb.js → review-telemetry-DACDMfqq.js} +4 -4
  30. package/dist/server/desktop-host.js +26 -38
  31. package/dist/{server-discovery-WmuFl2S3.js → server-discovery-CV5qf1J8.js} +2 -2
  32. package/dist/sharing/index.d.ts +137 -14
  33. package/dist/sharing/index.js +1 -1
  34. package/dist/{src-B2b8cCoI.js → src-BQDlJXkG.js} +44 -5
  35. package/dist/{src-DLWol7GZ.js → src-CrrW7lcY.js} +16 -1
  36. package/dist/{stored-document-migration-iwIQ4l5G.js → stored-document-migration-D1wK4yID.js} +3 -3
  37. package/dist/{thread-state-B7L1mpN_.js → thread-state-WTqSborE.js} +1 -1
  38. package/dist/{tool-failure-RLR8m9hq.js → tool-failure-YOFGHJCZ.js} +319 -55
  39. package/instructions/animation.md +69 -5
  40. package/instructions/animations/before-after-structure/README.md +27 -0
  41. package/instructions/animations/before-after-structure/animation.html +89 -0
  42. package/instructions/animations/before-after-structure/block.json +10 -0
  43. package/instructions/animations/before-after-structure/frames.json +313 -0
  44. package/instructions/animations/cache-fill-evict/README.md +24 -0
  45. package/instructions/animations/cache-fill-evict/animation.html +81 -0
  46. package/instructions/animations/cache-fill-evict/block.json +13 -0
  47. package/instructions/animations/cache-fill-evict/frames.json +79 -0
  48. package/instructions/animations/cache-fill-evict/scenarios.json +17 -0
  49. package/instructions/animations/log-replay/README.md +25 -0
  50. package/instructions/animations/log-replay/animation.html +86 -0
  51. package/instructions/animations/log-replay/block.json +10 -0
  52. package/instructions/animations/log-replay/frames.json +143 -0
  53. package/instructions/animations/log-replay/scenarios.json +18 -0
  54. package/instructions/animations/queue-workers/README.md +23 -0
  55. package/instructions/animations/queue-workers/animation.html +107 -0
  56. package/instructions/animations/queue-workers/block.json +13 -0
  57. package/instructions/animations/queue-workers/frames.json +119 -0
  58. package/instructions/animations/queue-workers/scenarios.json +18 -0
  59. package/instructions/animations/race-two-lanes/README.md +22 -0
  60. package/instructions/animations/race-two-lanes/animation.html +97 -0
  61. package/instructions/animations/race-two-lanes/block.json +1 -0
  62. package/instructions/animations/race-two-lanes/frames.json +234 -0
  63. package/instructions/animations/race-two-lanes/scenarios.json +19 -0
  64. package/instructions/animations/request-across-layers/README.md +24 -0
  65. package/instructions/animations/request-across-layers/animation.html +85 -0
  66. package/instructions/animations/request-across-layers/block.json +17 -0
  67. package/instructions/animations/request-across-layers/frames.json +58 -0
  68. package/instructions/animations/request-across-layers/scenarios.json +12 -0
  69. package/instructions/animations/request-timing/README.md +22 -0
  70. package/instructions/animations/request-timing/animation.html +120 -0
  71. package/instructions/animations/request-timing/block.json +21 -0
  72. package/instructions/animations/request-timing/markers.json +32 -0
  73. package/instructions/animations/request-timing/scenarios.json +4 -0
  74. package/instructions/animations/retry-backoff/README.md +22 -0
  75. package/instructions/animations/retry-backoff/animation.html +122 -0
  76. package/instructions/animations/retry-backoff/block.json +10 -0
  77. package/instructions/animations/retry-backoff/frames.json +65 -0
  78. package/instructions/animations/retry-backoff/scenarios.json +12 -0
  79. package/instructions/animations/state-machine/README.md +24 -0
  80. package/instructions/animations/state-machine/animation.html +95 -0
  81. package/instructions/animations/state-machine/block.json +9 -0
  82. package/instructions/animations/state-machine/frames.json +79 -0
  83. package/instructions/animations/state-machine/scenarios.json +12 -0
  84. package/instructions/animations/video-platform-architecture/README.md +24 -0
  85. package/instructions/animations/video-platform-architecture/animation.html +76 -0
  86. package/instructions/animations/video-platform-architecture/block.json +17 -0
  87. package/instructions/animations/video-platform-architecture/frames.json +97 -0
  88. package/instructions/animations/video-platform-architecture/scenarios.json +28 -0
  89. package/instructions/whiteboard.d.ts +168 -14
  90. package/package.json +6 -6
  91. package/src/animation-stage-kit.ts +150 -10
  92. package/src/cli-runner.ts +4 -1
  93. package/src/fixtures/blocks/clip.json +19 -0
  94. package/src/review-api/README.md +1 -0
  95. package/src/review-api/agent-cli.ts +3 -2
  96. package/src/review-api/animation-drafts.ts +151 -47
  97. package/src/review-api/animation-references.ts +92 -0
  98. package/src/review-api/animation-render.ts +106 -0
  99. package/src/review-api/authoring-tools.ts +10 -3
  100. package/src/review-api/document-text.ts +27 -12
  101. package/src/review-api/http.ts +52 -6
  102. package/src/review-api/instructions.ts +1 -1
  103. package/src/review-api/mcp.ts +10 -5
  104. package/src/review-api/public-tools.ts +71 -2
  105. package/src/review-api/store.ts +89 -12
  106. package/src/server/desktop-server.ts +17 -5
  107. package/src/server/headless-host.ts +4 -0
  108. package/src/server/review-gateway.ts +1 -0
  109. package/src/server/review-server-core.ts +71 -12
  110. package/src/sharing/client.ts +129 -32
  111. package/src/sharing/export.ts +42 -5
  112. package/src/sharing/file.ts +8 -0
  113. package/src/sharing/host.ts +202 -73
  114. package/src/sharing/import.ts +125 -33
  115. package/src/ui-telemetry-events.ts +25 -1
  116. package/dist/agent-cli-B8KBHuh_.js +0 -2
  117. package/dist/profile-BjcXi-oH.js +0 -2
@@ -1,21 +1,21 @@
1
1
  import { b as resolveRepoContext, d as git, r as defaultBranch, w as runVcs } from "./src-Y30gweiK.js";
2
2
  import { c as jsonString, m as isStringValue, n as isJsonObject, p as isObjectValue, u as parseJsonText } from "./src-X9phtB2j.js";
3
- import { _ as readBoundedStream, a as forgetSharedCommits, b as shareBundleSchema, c as ShareClient, d as legacyModeSchema, f as lensReport, g as StreamLimitError, h as uncategorizedReport, l as exportShare, m as reviewProgress, n as needsRepository, o as readShareRepository, p as progressUpdateSchema, r as shareKey, s as ShareAuthError, t as importShare, u as validateShareSources, x as shareIdSchema, y as parseShareLink } from "./import-YAIU6rB0.js";
4
- import { I as stepSources, T as selectionReferences, U as animationCode, et as formatAnchor, n as anchorPins, o as boardRepositoryId, ot as ReviewInputError, rt as selectionKey, u as elements } from "./src-B2b8cCoI.js";
5
- import { c as invalidateStructuralComparisons, d as readGitHubPullRequests, i as inspectSnapshot, l as diffrExecutable, r as commandSchema, u as diffrMissingError } from "./local-data-C58vzLZR.js";
6
- import { I as decodeStructuralDiffEvent, N as reviewDiffrLoginsSchema, O as ReviewFeedbackContactSchema, P as reviewDiffrSummarizerInputSchema, a as parseReviewVerbRequest, c as boardIconKind, i as parseReviewDesktopVerbResult, j as ReviewOnboardingRunRequestSchema, k as ReviewFeedbackSchema, l as boardTitle, m as REVIEW_CLIENT_REMOTE, p as REVIEW_CLIENT_HEADER, y as REVIEW_HOST_HEADER } from "./src-DLWol7GZ.js";
7
- import { B as TraceStorageDeniedError, D as listReviewTraceSessions, I as isTraceStorageConfigured, O as loadReviewAgentTrace, R as resolveTraceStorage, T as traceMachineEnabled, at as readStoreAuth, et as shellQuote, it as clearStoreAuth, ot as runStoreLogin, rt as DEFAULT_STORE_ORIGIN, z as selectTraceStorage } from "./review-home-paths-LdGS4SpR.js";
8
- import { D as scopedCoverage, f as sourceBranchName, l as pullRequestKey, p as pullRequestProvider } from "./stored-document-migration-iwIQ4l5G.js";
3
+ import { C as StreamLimitError, D as shareIdSchema, E as shareBundleSchema, S as uncategorizedReport, T as parseShareLink, _ as validateShareSources, a as shareKey, b as progressUpdateSchema, c as readShareRepository, d as ShareRequestError, f as parseWhiteboardFile, g as exportShare, h as whiteboardFileText, i as receiveShare, l as ShareAuthError, m as whiteboardFileName, n as needsRepository, p as whiteboardFileKey, r as planShareCopy, s as forgetSharedCommits, t as importShare, u as ShareClient, v as legacyModeSchema, w as readBoundedStream, x as reviewProgress, y as lensReport } from "./import-CZ1958L4.js";
4
+ import { I as stepSources, T as selectionReferences, U as animationCode, et as formatAnchor, n as anchorPins, o as boardRepositoryId, ot as ReviewInputError, rt as selectionKey, u as elements } from "./src-BQDlJXkG.js";
5
+ import { c as invalidateStructuralComparisons, d as readGitHubPullRequests, i as inspectSnapshot, l as diffrExecutable, r as commandSchema, u as diffrMissingError } from "./local-data-DdFkxFK_.js";
6
+ import { A as ReviewFeedbackSchema, F as reviewDiffrSummarizerInputSchema, L as decodeStructuralDiffEvent, M as ReviewOnboardingRunRequestSchema, P as reviewDiffrLoginsSchema, a as parseReviewVerbRequest, c as boardIconKind, i as parseReviewDesktopVerbResult, k as ReviewFeedbackContactSchema, l as boardTitle, m as REVIEW_CLIENT_REMOTE, p as REVIEW_CLIENT_HEADER, y as REVIEW_HOST_HEADER } from "./src-CrrW7lcY.js";
7
+ import { B as TraceStorageDeniedError, D as listReviewTraceSessions, I as isTraceStorageConfigured, O as loadReviewAgentTrace, R as resolveTraceStorage, T as traceMachineEnabled, at as readStoreAuth, et as shellQuote, it as clearStoreAuth, ot as runStoreLogin, rt as DEFAULT_STORE_ORIGIN, z as selectTraceStorage } from "./review-home-paths-BmZX2qiz.js";
8
+ import { D as scopedCoverage, f as sourceBranchName, l as pullRequestKey, p as pullRequestProvider } from "./stored-document-migration-D1wK4yID.js";
9
9
  import { A as processStartIdentity, L as writePrivateJsonAtomic, N as devReviewHome, h as TraceConfigurationError, i as isS3MockMode, j as withFileLock } from "./s3-config-CjcLyFV5.js";
10
10
  import { t as errorMessage } from "./error-message-OtiDonty.js";
11
- import { i as readReviewPackageVersion, n as readBuildCommit, t as findReviewPackageRoot } from "./package-paths-Cg6fnaGp.js";
12
- import { _ as reviewInstanceIdentity, n as desktopOwnsHome, o as reviewServerDiscoveryPath, p as desktopUsesHome, r as headlessServerLockPath, t as DESKTOP_TAKEOVER_SIGNAL } from "./server-discovery-WmuFl2S3.js";
13
- import { c as askPicksSchema, d as selectionMarkdown, l as askQuestionSchema, n as askAgentIds, r as askChoiceKinds, s as askPermissionsSchema, t as applyAskChange, u as AgentSelectionSchema } from "./thread-state-B7L1mpN_.js";
14
- import { t as openReviewProfile } from "./profile--BYIjPkc.js";
15
- import { c as REVIEW_APP_SESSION_ID_HEADER, d as sanitizeUiTelemetryEvent, f as cleanTelemetryText, o as reviewRequestOrigin, r as REVIEW_PERSON_HEADER, s as BUNDLE_FRAME_PATTERN, t as ASK_THREAD_ENV, u as isReportableCleanedMessage } from "./request-origin-B3_XI2VA.js";
16
- import { a as defaultAskPermissions, i as askAgents, l as launchAskAgent, n as askAgentSettings, o as detectAskAgents, r as askAgentTakesMcp, t as fuzzyRank } from "./fuzzy-match-DZjeOaPY.js";
17
- import { a as inspectQuerySchema, i as renderInstructions, o as queryAnchor, r as instructionsQuerySchema, s as readQuerySchemas, t as authoringTools } from "./authoring-tools-CiLy46kh.js";
18
- import { a as headlessServerOwner, c as stopBackgroundServer, o as recordedBackgroundServer } from "./background-server-eZWXoVcI.js";
11
+ import { i as readReviewPackageVersion, n as readBuildCommit, t as findReviewPackageRoot } from "./package-paths-Dk7MOXxN.js";
12
+ import { _ as reviewInstanceIdentity, n as desktopOwnsHome, o as reviewServerDiscoveryPath, p as desktopUsesHome, r as headlessServerLockPath, t as DESKTOP_TAKEOVER_SIGNAL } from "./server-discovery-CV5qf1J8.js";
13
+ import { c as askPicksSchema, d as selectionMarkdown, l as askQuestionSchema, n as askAgentIds, r as askChoiceKinds, s as askPermissionsSchema, t as applyAskChange, u as AgentSelectionSchema } from "./thread-state-WTqSborE.js";
14
+ import { t as openReviewProfile } from "./profile-Bsgzi9PA.js";
15
+ import { c as REVIEW_APP_SESSION_ID_HEADER, d as sanitizeUiTelemetryEvent, f as cleanTelemetryText, o as reviewRequestOrigin, r as REVIEW_PERSON_HEADER, s as BUNDLE_FRAME_PATTERN, t as ASK_THREAD_ENV, u as isReportableCleanedMessage } from "./request-origin-D6B-zrL8.js";
16
+ import { a as defaultAskPermissions, i as askAgents, l as launchAskAgent, n as askAgentSettings, o as detectAskAgents, r as askAgentTakesMcp, t as fuzzyRank } from "./fuzzy-match-CxcsLQGx.js";
17
+ import { a as inspectQuerySchema, i as renderInstructions, o as queryAnchor, r as instructionsQuerySchema, s as readQuerySchemas, t as authoringTools } from "./authoring-tools-CkfxTVJm.js";
18
+ import { a as headlessServerOwner, c as stopBackgroundServer, o as recordedBackgroundServer } from "./background-server-BraP8pJ9.js";
19
19
  import crypto$1, { createHash, randomBytes, randomUUID } from "node:crypto";
20
20
  import { z } from "zod";
21
21
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
@@ -28,9 +28,9 @@ import { setTimeout as setTimeout$1 } from "node:timers/promises";
28
28
  import { promisify } from "node:util";
29
29
  import { fileURLToPath, pathToFileURL } from "node:url";
30
30
  import { PROTOCOL_VERSION, RequestError, client, methods } from "@agentclientprotocol/sdk";
31
+ import { getRequestListener } from "@hono/node-server";
31
32
  import { Hono } from "hono";
32
33
  import { stringify } from "smol-toml";
33
- import { getRequestListener } from "@hono/node-server";
34
34
  import { createServer } from "node:http";
35
35
  import { streamSSE } from "hono/streaming";
36
36
  import { once } from "node:events";
@@ -1404,6 +1404,399 @@ var AskThreads = class {
1404
1404
  }
1405
1405
  };
1406
1406
  //#endregion
1407
+ //#region src/review-preferences.ts
1408
+ /**
1409
+ * Machine-wide Review preferences the server itself needs. Workbench settings
1410
+ * do not work here: the review server never reads the workbench
1411
+ * configuration.
1412
+ */
1413
+ function reviewPreferencesPath(devHome = devReviewHome()) {
1414
+ return path.join(devHome, "preferences.json");
1415
+ }
1416
+ /** Whether agents are offered animation authoring; existing animations
1417
+ * always render. A missing or unreadable file means off; it never throws. */
1418
+ async function readAnimationsEnabled(devHome) {
1419
+ try {
1420
+ const raw = parseJsonText(await readFile(reviewPreferencesPath(devHome), "utf8"));
1421
+ return isJsonObject(raw) && raw.animationsEnabled === true;
1422
+ } catch {
1423
+ return false;
1424
+ }
1425
+ }
1426
+ async function writeAnimationsEnabled(enabled, devHome) {
1427
+ await writePrivateJsonAtomic(reviewPreferencesPath(devHome), { animationsEnabled: enabled });
1428
+ return enabled;
1429
+ }
1430
+ //#endregion
1431
+ //#region src/server/http-json.ts
1432
+ const DEFAULT_MAX_REQUEST_BYTES = 1048576;
1433
+ var HttpJsonError = class extends Error {
1434
+ statusCode;
1435
+ constructor(message, statusCode) {
1436
+ super(message);
1437
+ this.statusCode = statusCode;
1438
+ this.name = "HttpJsonError";
1439
+ }
1440
+ };
1441
+ /** An error whose status and machine code the JSON routes hand straight back. */
1442
+ var ReviewServerError = class extends Error {
1443
+ statusCode;
1444
+ code;
1445
+ name = "ReviewServerError";
1446
+ constructor(message, statusCode, code) {
1447
+ super(message);
1448
+ this.statusCode = statusCode;
1449
+ this.code = code;
1450
+ }
1451
+ };
1452
+ //#endregion
1453
+ //#region src/server/hono-http.ts
1454
+ const H2_FORBIDDEN_HEADERS = [
1455
+ "connection",
1456
+ "keep-alive",
1457
+ "proxy-connection",
1458
+ "transfer-encoding",
1459
+ "upgrade"
1460
+ ];
1461
+ function createNodeRequestListener(app) {
1462
+ return getRequestListener(async (request, env) => {
1463
+ const response = await app.fetch(request, env);
1464
+ if (env.incoming.httpVersionMajor !== 2 || !H2_FORBIDDEN_HEADERS.some((name) => response.headers.has(name))) return response;
1465
+ const stripped = new Response(response.body, response);
1466
+ for (const name of H2_FORBIDDEN_HEADERS) stripped.headers.delete(name);
1467
+ return stripped;
1468
+ });
1469
+ }
1470
+ function jsonResponse(body, status, options = {}) {
1471
+ const headers = new Headers({ "content-type": options.contentType ?? "application/json; charset=utf-8" });
1472
+ if (options.cacheControl) headers.set("cache-control", options.cacheControl);
1473
+ const serialized = JSON.stringify(body);
1474
+ return new Response(options.newline === false ? serialized : `${serialized}\n`, {
1475
+ status,
1476
+ headers
1477
+ });
1478
+ }
1479
+ function applyCorsHeaders(request, response) {
1480
+ const origin = request.headers.get("origin");
1481
+ if (origin) {
1482
+ response.headers.set("access-control-allow-origin", origin);
1483
+ const vary = response.headers.get("vary");
1484
+ const varyFields = vary?.split(",").map((field) => field.trim().toLowerCase());
1485
+ if (!varyFields?.includes("*") && !varyFields?.includes("origin")) response.headers.set("vary", vary ? `${vary}, Origin` : "Origin");
1486
+ }
1487
+ response.headers.set("access-control-allow-headers", `content-type, x-review-token, ${REVIEW_PERSON_HEADER}, ${REVIEW_APP_SESSION_ID_HEADER}`);
1488
+ response.headers.set("access-control-allow-methods", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS");
1489
+ response.headers.set("access-control-allow-private-network", "true");
1490
+ response.headers.set("access-control-expose-headers", REVIEW_HOST_HEADER);
1491
+ return response;
1492
+ }
1493
+ /** The allowed origin, headers and methods hold for the server's lifetime, so
1494
+ * the client may reuse a preflight. Chromium otherwise keeps one for 5 s, and
1495
+ * nearly every request paid a second round trip. Two hours is Chromium's cap. */
1496
+ const PREFLIGHT_MAX_AGE_SECONDS = 7200;
1497
+ function corsPreflightResponse(request) {
1498
+ const response = applyCorsHeaders(request, new Response(null, { status: 204 }));
1499
+ response.headers.set("access-control-max-age", String(PREFLIGHT_MAX_AGE_SECONDS));
1500
+ return response;
1501
+ }
1502
+ function isAuthorizedRequest(request, expectedToken) {
1503
+ const supplied = request.headers.get("x-review-token") ?? new URL(request.url).searchParams.get("token");
1504
+ if (!supplied) return false;
1505
+ const expected = Buffer.from(expectedToken);
1506
+ const actual = Buffer.from(supplied);
1507
+ return expected.length === actual.length && crypto$1.timingSafeEqual(expected, actual);
1508
+ }
1509
+ async function readBoundedRequestJson(request, maxBytes = DEFAULT_MAX_REQUEST_BYTES, emptyValue, options = {}) {
1510
+ assertJsonContentType(request, options);
1511
+ const contentLength = Number(request.headers.get("content-length"));
1512
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) throw requestTooLarge(maxBytes);
1513
+ let body;
1514
+ try {
1515
+ body = request.body ? (await readBoundedStream(request.body, maxBytes)).toString("utf8") : "";
1516
+ } catch (error) {
1517
+ if (error instanceof StreamLimitError) throw requestTooLarge(maxBytes);
1518
+ throw error;
1519
+ }
1520
+ if (!body && emptyValue !== void 0) return emptyValue;
1521
+ try {
1522
+ return parseJsonText(body);
1523
+ } catch {
1524
+ throw new HttpJsonError("Invalid JSON body.", 400);
1525
+ }
1526
+ }
1527
+ function assertJsonContentType(request, options) {
1528
+ const mediaType = request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
1529
+ if (mediaType !== "application/json" && !mediaType?.endsWith("+json") && !(options.allowTextPlain && mediaType === "text/plain")) throw new HttpJsonError("Content-Type must be application/json.", 415);
1530
+ }
1531
+ function requestTooLarge(maxBytes) {
1532
+ return new HttpJsonError(`Request body exceeds ${maxBytes === 1048576 ? "1 MiB" : `${maxBytes} bytes`}.`, 413);
1533
+ }
1534
+ //#endregion
1535
+ //#region src/sharing/auth.ts
1536
+ /** CI credentials stay in the server environment and are never persisted to a profile. */
1537
+ async function readSharingAuth(env = process.env) {
1538
+ if (env.DEV_REVIEW_SHARE_TOKEN === void 0) {
1539
+ if (env.DEV_REVIEW_SHARE_ORIGIN !== void 0) throw new ReviewInputError("DEV_REVIEW_SHARE_ORIGIN requires DEV_REVIEW_SHARE_TOKEN.");
1540
+ return readStoreAuth(env);
1541
+ }
1542
+ const token = env.DEV_REVIEW_SHARE_TOKEN.trim();
1543
+ if (!token) throw new ReviewInputError("DEV_REVIEW_SHARE_TOKEN is empty.");
1544
+ let url;
1545
+ try {
1546
+ url = new URL(env.DEV_REVIEW_SHARE_ORIGIN ?? DEFAULT_STORE_ORIGIN);
1547
+ } catch {
1548
+ throw new ReviewInputError("DEV_REVIEW_SHARE_ORIGIN must be a bare HTTPS origin.");
1549
+ }
1550
+ if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw new ReviewInputError("DEV_REVIEW_SHARE_ORIGIN must be a bare HTTPS origin.");
1551
+ return {
1552
+ origin: url.origin,
1553
+ token
1554
+ };
1555
+ }
1556
+ //#endregion
1557
+ //#region src/sharing/host.ts
1558
+ const publishSchema = z.strictObject({
1559
+ reviewId: z.string().min(1),
1560
+ version: z.number().int().nonnegative().optional(),
1561
+ requestId: z.uuid().optional()
1562
+ });
1563
+ const importSchema = z.union([z.strictObject({ url: z.string().max(2048) }), z.strictObject({ path: z.string().min(1).max(4096) })]);
1564
+ /** Mounted behind local host authentication. Account credentials never enter the renderer. */
1565
+ function mountSharingHost(app, store, data, options = {}) {
1566
+ const startLogin = options.login ?? runStoreLogin;
1567
+ let login = { pending: false };
1568
+ let warmedAt = 0;
1569
+ const readAccount = async () => {
1570
+ const account = await readStoreAuth();
1571
+ if (account && Date.now() - warmedAt > 6e4) {
1572
+ warmedAt = Date.now();
1573
+ new ShareClient(account.origin, account.token, options.fetch).warm().catch(() => {});
1574
+ }
1575
+ return {
1576
+ account: account ? {
1577
+ login: account.login,
1578
+ origin: account.origin,
1579
+ email: account.email,
1580
+ githubLogin: account.githubLogin
1581
+ } : null,
1582
+ ...login
1583
+ };
1584
+ };
1585
+ app.get("/sharing/account", async (context) => context.json(await readAccount()));
1586
+ app.post("/sharing/logout", async (context) => {
1587
+ await clearStoreAuth();
1588
+ return context.json(await readAccount());
1589
+ });
1590
+ app.post("/sharing/login", async (context) => {
1591
+ if (!login.pending) {
1592
+ login = { pending: true };
1593
+ options.onLogin?.("started");
1594
+ const discard = new Writable({ write(_chunk, _encoding, done) {
1595
+ done();
1596
+ } });
1597
+ startLogin({
1598
+ traces: false,
1599
+ stdout: discard,
1600
+ stderr: discard,
1601
+ openUrl: options.openUrl,
1602
+ onDeviceCode: (device) => {
1603
+ Object.assign(login, device);
1604
+ }
1605
+ }).then((code) => {
1606
+ login = { pending: false };
1607
+ if (code) login.error = "Sign-in did not finish. Try again.";
1608
+ options.onLogin?.(code ? "failed" : "succeeded", code ? "did_not_finish" : void 0);
1609
+ }).catch(() => {
1610
+ login = {
1611
+ pending: false,
1612
+ error: "Sign-in failed. Try again."
1613
+ };
1614
+ options.onLogin?.("failed", "error");
1615
+ });
1616
+ }
1617
+ return context.json(login);
1618
+ });
1619
+ mountSharingPublisher(app, store, data, options);
1620
+ const imports = /* @__PURE__ */ new Map();
1621
+ let importing = Promise.resolve();
1622
+ const serially = (task) => {
1623
+ const run = importing.then(task);
1624
+ importing = run.then(() => {}, () => {});
1625
+ return run;
1626
+ };
1627
+ app.get("/sharing/import/:id", (context) => {
1628
+ const id = context.req.param("id");
1629
+ const { reviewId = id, ...status } = imports.get(id) ?? {
1630
+ stage: "error",
1631
+ error: "Shared review is not available."
1632
+ };
1633
+ return context.json({
1634
+ reviewId,
1635
+ ...status,
1636
+ ...status.stage === "ready" && { title: store.read(reviewId).title }
1637
+ });
1638
+ });
1639
+ const shareInput = (onStage) => ({
1640
+ store,
1641
+ data,
1642
+ fetchRepository: options.fetchRepository,
1643
+ cloneRepository: options.cloneRepository,
1644
+ onStage
1645
+ });
1646
+ const startImport = (id, initial, receive) => {
1647
+ const known = imports.get(id)?.stage;
1648
+ if (!known || known === "ready" || known === "error") {
1649
+ const status = { stage: initial };
1650
+ imports.set(id, status);
1651
+ receive((stage) => {
1652
+ status.stage = stage;
1653
+ }).then((reviewId) => {
1654
+ status.reviewId = reviewId;
1655
+ status.stage = "ready";
1656
+ }).catch((error) => {
1657
+ status.stage = "error";
1658
+ status.error = error instanceof ReviewInputError || error instanceof ShareRequestError && error.status !== 404 ? error.message : "This share is unavailable, revoked, or could not be read.";
1659
+ });
1660
+ }
1661
+ const { reviewId: _reviewId, ...status } = imports.get(id);
1662
+ return {
1663
+ reviewId: id,
1664
+ ...status
1665
+ };
1666
+ };
1667
+ app.post("/sharing/import", async (context) => {
1668
+ const input = importSchema.parse(await readBoundedRequestJson(context.req.raw));
1669
+ if ("path" in input) {
1670
+ if (!path.isAbsolute(input.path)) throw new ReviewInputError("Use an absolute file path.", 400);
1671
+ const text = await readFile(input.path, "utf8").catch(() => {
1672
+ throw new ReviewInputError(`Could not read ${path.basename(input.path)}. Check that the file still exists and Whiteboard can open it.`, 400);
1673
+ });
1674
+ const key = whiteboardFileKey(text);
1675
+ const copy = () => store.copiesOf(key)[0]?.reviewId;
1676
+ return context.json(startImport(key, "reading", async (onStage) => copy() ?? serially(async () => copy() ?? importShare(shareInput(onStage), key, parseWhiteboardFile(text)))), 202);
1677
+ }
1678
+ const parsed = parseShareLink(input.url);
1679
+ const account = await readStoreAuth();
1680
+ if (parsed.origin !== DEFAULT_STORE_ORIGIN && parsed.origin !== account?.origin) throw new ReviewInputError("This share uses an untrusted service. Sign in to that service before opening its links.", 400);
1681
+ const { origin, capability, revision } = parsed;
1682
+ const key = shareKey(origin, capability);
1683
+ const client = new ShareClient(origin, void 0, options.fetch);
1684
+ return context.json(startImport(revision ? `${key}-${revision}` : key, "downloading", async (onStage) => {
1685
+ let meta;
1686
+ try {
1687
+ meta = await client.shared(capability, revision);
1688
+ } catch (error) {
1689
+ const copies = store.copiesOf(key);
1690
+ const copy = copies.find(({ shared }) => revision ? shared.revisionId === revision : !shared.pinned) ?? copies[0];
1691
+ if (!copy) throw error;
1692
+ return copy.reviewId;
1693
+ }
1694
+ const pinned = revision !== void 0;
1695
+ const plan = planShareCopy(store.copiesOf(key), meta, pinned);
1696
+ return plan.kind === "open" ? plan.reviewId : serially(() => receiveShare(shareInput(onStage), key, meta, () => client.download(capability, meta.revision.id), pinned));
1697
+ }), 202);
1698
+ });
1699
+ }
1700
+ const preparedShareSchema = z.strictObject({
1701
+ title: z.string(),
1702
+ version: z.number().int().nonnegative(),
1703
+ bundle: shareBundleSchema,
1704
+ needsRepository: z.boolean()
1705
+ });
1706
+ function remoteShare(isRemote, fetch) {
1707
+ return ({ reviewId, version }) => isRemote(reviewId) ? (async () => {
1708
+ const response = await fetch(new Request(`http://whiteboard/reviews-api/${encodeURIComponent(reviewId)}/share-bundle`, {
1709
+ method: "POST",
1710
+ headers: { "content-type": "application/json" },
1711
+ body: JSON.stringify({ version })
1712
+ }));
1713
+ const body = await response.json().catch(() => void 0);
1714
+ if (!response.ok) throw new ReviewInputError(z.object({ error: z.string() }).safeParse(body).data?.error ?? "Could not read the board from its host.", response.status === 400 ? 400 : 409);
1715
+ return preparedShareSchema.parse(body);
1716
+ })() : void 0;
1717
+ }
1718
+ /** Publishing needs only an authored store, never Desktop or recipient workspaces. */
1719
+ function mountSharingPublisher(app, store, data, options = {}) {
1720
+ const prepareLocal = async (input) => {
1721
+ const snapshot = store.read(input.reviewId, input.version);
1722
+ if (snapshot.target?.kind !== "worktree") validateShareSources(snapshot);
1723
+ const repositoryId = snapshot.pins?.repositoryId ?? snapshot.target?.repositoryId;
1724
+ const root = repositoryId ? store.repositoryPath(repositoryId) : void 0;
1725
+ const repository = root ? (options.readRepository ?? readShareRepository)(root).catch(() => void 0) : void 0;
1726
+ const bundle = await exportShare({
1727
+ store,
1728
+ data,
1729
+ reviewId: snapshot.reviewId,
1730
+ version: snapshot.version
1731
+ });
1732
+ const verified = await repository;
1733
+ if (verified) bundle.repository = verified;
1734
+ return {
1735
+ title: snapshot.title,
1736
+ version: snapshot.version,
1737
+ bundle,
1738
+ needsRepository: needsRepository(bundle, snapshot)
1739
+ };
1740
+ };
1741
+ const prepare = (input) => options.remoteShare?.(input) ?? prepareLocal(input);
1742
+ app.post("/:id/share-bundle", async (context) => {
1743
+ const { version } = publishSchema.pick({ version: true }).parse(await readBoundedRequestJson(context.req.raw));
1744
+ return context.json(await prepareLocal({
1745
+ reviewId: context.req.param("id"),
1746
+ version
1747
+ }));
1748
+ });
1749
+ app.post("/sharing/export", async (context) => {
1750
+ const { title, bundle, needsRepository } = await prepare(publishSchema.parse(await readBoundedRequestJson(context.req.raw)));
1751
+ if (bundle.frozen) throw new ReviewInputError("Commit your changes to export this whiteboard as a file.");
1752
+ else if (!bundle.repository && needsRepository) throw new ReviewInputError("Exporting pinned code needs a GitHub remote the file's reader can fetch from.");
1753
+ return context.json({
1754
+ filename: whiteboardFileName(title),
1755
+ text: whiteboardFileText(bundle)
1756
+ });
1757
+ });
1758
+ app.post("/sharing/publish", async (context) => {
1759
+ const input = publishSchema.parse(await readBoundedRequestJson(context.req.raw));
1760
+ const account = await readSharingAuth();
1761
+ if (!account) throw new ReviewInputError("Set DEV_REVIEW_SHARE_TOKEN for CI, or run whiteboard login before sharing.", 409);
1762
+ const { bundle, version } = await prepare(input);
1763
+ try {
1764
+ const result = await new ShareClient(account.origin, account.token, options.fetch).create(bundle, input.requestId ?? randomUUID());
1765
+ const url = new URL(result.url);
1766
+ if (process.env.DEV_FAST_REVIEW_APP_URL_PROTOCOL === "dev-fast-review-preview") url.searchParams.set("app", "preview");
1767
+ options.onPublished?.({
1768
+ reviewId: input.reviewId,
1769
+ version
1770
+ });
1771
+ return context.json({
1772
+ ...result,
1773
+ url: url.href,
1774
+ version
1775
+ });
1776
+ } catch (error) {
1777
+ if (error instanceof ReviewInputError) throw error;
1778
+ if (error instanceof ShareRequestError) throw new ReviewInputError(error.message, 409);
1779
+ if (error instanceof ShareAuthError) {
1780
+ if (process.env.DEV_REVIEW_SHARE_TOKEN === void 0) await clearStoreAuth();
1781
+ throw new ReviewInputError("Your sign-in has expired. Sign in again to share.", 401);
1782
+ }
1783
+ if (error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError")) throw new ReviewInputError("The sharing request timed out. Please retry.", 409);
1784
+ throw new ReviewInputError(error instanceof TypeError ? "Could not reach the sharing service. Check your connection, then retry." : "The sharing service returned an unexpected response. Update Whiteboard or report this issue.", 409);
1785
+ }
1786
+ });
1787
+ app.post("/sharing/revoke", async (context) => {
1788
+ const { shareId } = z.strictObject({ shareId: shareIdSchema }).parse(await readBoundedRequestJson(context.req.raw));
1789
+ const account = await readSharingAuth();
1790
+ if (!account) throw new ReviewInputError("Set DEV_REVIEW_SHARE_TOKEN for CI, or run whiteboard login first.", 409);
1791
+ await new ShareClient(account.origin, account.token, options.fetch).revoke(shareId);
1792
+ options.onRevoked?.({ shareId });
1793
+ return context.json({
1794
+ shareId,
1795
+ revoked: true
1796
+ });
1797
+ });
1798
+ }
1799
+ //#endregion
1407
1800
  //#region src/server/diffr-config.ts
1408
1801
  const execFileAsync = promisify(execFile);
1409
1802
  let writes = Promise.resolve();
@@ -1839,130 +2232,26 @@ var GlobalReviewDesktopVerbRelay = class {
1839
2232
  const listener = this.clients.get(writer);
1840
2233
  if (!listener) return;
1841
2234
  writer.signal.removeEventListener("abort", listener);
1842
- this.clients.delete(writer);
1843
- for (const [id, verb] of [...this.pending]) if (verb.waiting.get(id) === writer) this.stopWaiting(verb, id);
1844
- if (this.clients.size === 0) this.notifyAttached();
1845
- }
1846
- /** Drops one client's id; with nobody left to answer, the verb fails. */
1847
- stopWaiting(verb, id) {
1848
- verb.waiting.delete(id);
1849
- this.pending.delete(id);
1850
- if (verb.waiting.size === 0) this.settle(verb, verb.lastFailure ?? {
1851
- ok: false,
1852
- error: NOT_ATTACHED
1853
- });
1854
- }
1855
- settle(verb, response) {
1856
- for (const id of verb.waiting.keys()) this.pending.delete(id);
1857
- verb.waiting.clear();
1858
- clearTimeout(verb.timer);
1859
- verb.resolve(response);
1860
- }
1861
- };
1862
- //#endregion
1863
- //#region src/server/http-json.ts
1864
- const DEFAULT_MAX_REQUEST_BYTES = 1048576;
1865
- var HttpJsonError = class extends Error {
1866
- statusCode;
1867
- constructor(message, statusCode) {
1868
- super(message);
1869
- this.statusCode = statusCode;
1870
- this.name = "HttpJsonError";
1871
- }
1872
- };
1873
- /** An error whose status and machine code the JSON routes hand straight back. */
1874
- var ReviewServerError = class extends Error {
1875
- statusCode;
1876
- code;
1877
- name = "ReviewServerError";
1878
- constructor(message, statusCode, code) {
1879
- super(message);
1880
- this.statusCode = statusCode;
1881
- this.code = code;
1882
- }
1883
- };
1884
- //#endregion
1885
- //#region src/server/hono-http.ts
1886
- const H2_FORBIDDEN_HEADERS = [
1887
- "connection",
1888
- "keep-alive",
1889
- "proxy-connection",
1890
- "transfer-encoding",
1891
- "upgrade"
1892
- ];
1893
- function createNodeRequestListener(app) {
1894
- return getRequestListener(async (request, env) => {
1895
- const response = await app.fetch(request, env);
1896
- if (env.incoming.httpVersionMajor !== 2 || !H2_FORBIDDEN_HEADERS.some((name) => response.headers.has(name))) return response;
1897
- const stripped = new Response(response.body, response);
1898
- for (const name of H2_FORBIDDEN_HEADERS) stripped.headers.delete(name);
1899
- return stripped;
1900
- });
1901
- }
1902
- function jsonResponse(body, status, options = {}) {
1903
- const headers = new Headers({ "content-type": options.contentType ?? "application/json; charset=utf-8" });
1904
- if (options.cacheControl) headers.set("cache-control", options.cacheControl);
1905
- const serialized = JSON.stringify(body);
1906
- return new Response(options.newline === false ? serialized : `${serialized}\n`, {
1907
- status,
1908
- headers
1909
- });
1910
- }
1911
- function applyCorsHeaders(request, response) {
1912
- const origin = request.headers.get("origin");
1913
- if (origin) {
1914
- response.headers.set("access-control-allow-origin", origin);
1915
- const vary = response.headers.get("vary");
1916
- const varyFields = vary?.split(",").map((field) => field.trim().toLowerCase());
1917
- if (!varyFields?.includes("*") && !varyFields?.includes("origin")) response.headers.set("vary", vary ? `${vary}, Origin` : "Origin");
1918
- }
1919
- response.headers.set("access-control-allow-headers", `content-type, x-review-token, ${REVIEW_PERSON_HEADER}, ${REVIEW_APP_SESSION_ID_HEADER}`);
1920
- response.headers.set("access-control-allow-methods", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS");
1921
- response.headers.set("access-control-allow-private-network", "true");
1922
- response.headers.set("access-control-expose-headers", REVIEW_HOST_HEADER);
1923
- return response;
1924
- }
1925
- /** The allowed origin, headers and methods hold for the server's lifetime, so
1926
- * the client may reuse a preflight. Chromium otherwise keeps one for 5 s, and
1927
- * nearly every request paid a second round trip. Two hours is Chromium's cap. */
1928
- const PREFLIGHT_MAX_AGE_SECONDS = 7200;
1929
- function corsPreflightResponse(request) {
1930
- const response = applyCorsHeaders(request, new Response(null, { status: 204 }));
1931
- response.headers.set("access-control-max-age", String(PREFLIGHT_MAX_AGE_SECONDS));
1932
- return response;
1933
- }
1934
- function isAuthorizedRequest(request, expectedToken) {
1935
- const supplied = request.headers.get("x-review-token") ?? new URL(request.url).searchParams.get("token");
1936
- if (!supplied) return false;
1937
- const expected = Buffer.from(expectedToken);
1938
- const actual = Buffer.from(supplied);
1939
- return expected.length === actual.length && crypto$1.timingSafeEqual(expected, actual);
1940
- }
1941
- async function readBoundedRequestJson(request, maxBytes = DEFAULT_MAX_REQUEST_BYTES, emptyValue, options = {}) {
1942
- assertJsonContentType(request, options);
1943
- const contentLength = Number(request.headers.get("content-length"));
1944
- if (Number.isFinite(contentLength) && contentLength > maxBytes) throw requestTooLarge(maxBytes);
1945
- let body;
1946
- try {
1947
- body = request.body ? (await readBoundedStream(request.body, maxBytes)).toString("utf8") : "";
1948
- } catch (error) {
1949
- if (error instanceof StreamLimitError) throw requestTooLarge(maxBytes);
1950
- throw error;
2235
+ this.clients.delete(writer);
2236
+ for (const [id, verb] of [...this.pending]) if (verb.waiting.get(id) === writer) this.stopWaiting(verb, id);
2237
+ if (this.clients.size === 0) this.notifyAttached();
1951
2238
  }
1952
- if (!body && emptyValue !== void 0) return emptyValue;
1953
- try {
1954
- return parseJsonText(body);
1955
- } catch {
1956
- throw new HttpJsonError("Invalid JSON body.", 400);
2239
+ /** Drops one client's id; with nobody left to answer, the verb fails. */
2240
+ stopWaiting(verb, id) {
2241
+ verb.waiting.delete(id);
2242
+ this.pending.delete(id);
2243
+ if (verb.waiting.size === 0) this.settle(verb, verb.lastFailure ?? {
2244
+ ok: false,
2245
+ error: NOT_ATTACHED
2246
+ });
1957
2247
  }
1958
- }
1959
- function assertJsonContentType(request, options) {
1960
- const mediaType = request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
1961
- if (mediaType !== "application/json" && !mediaType?.endsWith("+json") && !(options.allowTextPlain && mediaType === "text/plain")) throw new HttpJsonError("Content-Type must be application/json.", 415);
1962
- }
1963
- function requestTooLarge(maxBytes) {
1964
- return new HttpJsonError(`Request body exceeds ${maxBytes === 1048576 ? "1 MiB" : `${maxBytes} bytes`}.`, 413);
1965
- }
2248
+ settle(verb, response) {
2249
+ for (const id of verb.waiting.keys()) this.pending.delete(id);
2250
+ verb.waiting.clear();
2251
+ clearTimeout(verb.timer);
2252
+ verb.resolve(response);
2253
+ }
2254
+ };
1966
2255
  //#endregion
1967
2256
  //#region src/error-telemetry.ts
1968
2257
  /**
@@ -2225,6 +2514,52 @@ async function captureSanitizedUiTelemetry(telemetry, request, name, properties,
2225
2514
  }
2226
2515
  }
2227
2516
  //#endregion
2517
+ //#region src/review-api/animation-render.ts
2518
+ const renderQuerySchema = z.object({
2519
+ targetId: z.string().min(1),
2520
+ /** A frame key, or a clip's marker key; omitted, the first stop. */
2521
+ frame: z.string().min(1).optional()
2522
+ });
2523
+ /** What the Desktop answers: the stop it showed, and the figure's PNG. */
2524
+ const renderedFrameSchema = z.object({
2525
+ frame: z.object({
2526
+ key: z.string(),
2527
+ title: z.string(),
2528
+ position: z.number().int().nonnegative(),
2529
+ count: z.number().int().positive()
2530
+ }),
2531
+ scenario: z.string().optional(),
2532
+ /** The figure ran past the window, so the capture cuts it. */
2533
+ clipped: z.boolean(),
2534
+ /** The error the stage reported, if its script threw. */
2535
+ error: z.string().nullable(),
2536
+ /** The capture, base64. */
2537
+ png: z.string().min(1)
2538
+ });
2539
+ const isStageBlock = (element) => element.type === "animation" || element.type === "clip";
2540
+ /** The stop to render: `frame` checked against the block's frames or markers, or
2541
+ * the first stop of the first scenario when omitted. */
2542
+ function renderTarget(snapshot, targetId, frame) {
2543
+ const target = elements(snapshot.document).find((element) => element.id === targetId);
2544
+ if (!target) throw new ReviewInputError("Target not found in this version.", 404);
2545
+ if (!isStageBlock(target)) throw new ReviewInputError(`${targetId} is a ${target.type}; only an animation or a clip renders a frame.`);
2546
+ const noun = target.type === "clip" ? "marker" : "frame";
2547
+ const keys = target.type === "clip" ? target.markers.map((marker) => marker.key) : target.frames.map((item) => item.key);
2548
+ if (frame === void 0) {
2549
+ const first = target.type === "animation" ? target.scenarios?.[0]?.frames[0] ?? keys[0] : keys[0];
2550
+ if (first === void 0) throw new ReviewInputError(`${targetId} has no ${noun}s to render.`);
2551
+ return {
2552
+ type: target.type,
2553
+ key: first
2554
+ };
2555
+ }
2556
+ if (!keys.includes(frame)) throw new ReviewInputError(`${frame} is not a ${noun} of ${targetId}. Its ${noun}s: ${keys.join(", ")}.`);
2557
+ return {
2558
+ type: target.type,
2559
+ key: frame
2560
+ };
2561
+ }
2562
+ //#endregion
2228
2563
  //#region src/ask/checkout-files.ts
2229
2564
  /** The checkout's files relative to its root: tracked ones, and new ones git
2230
2565
  * does not ignore. None where git cannot list them. */
@@ -2579,259 +2914,6 @@ async function defaultRunGitHubApi(host, endpoint) {
2579
2914
  return stdout;
2580
2915
  }
2581
2916
  //#endregion
2582
- //#region src/sharing/auth.ts
2583
- /** CI credentials stay in the server environment and are never persisted to a profile. */
2584
- async function readSharingAuth(env = process.env) {
2585
- if (env.DEV_REVIEW_SHARE_TOKEN === void 0) {
2586
- if (env.DEV_REVIEW_SHARE_ORIGIN !== void 0) throw new ReviewInputError("DEV_REVIEW_SHARE_ORIGIN requires DEV_REVIEW_SHARE_TOKEN.");
2587
- return readStoreAuth(env);
2588
- }
2589
- const token = env.DEV_REVIEW_SHARE_TOKEN.trim();
2590
- if (!token) throw new ReviewInputError("DEV_REVIEW_SHARE_TOKEN is empty.");
2591
- let url;
2592
- try {
2593
- url = new URL(env.DEV_REVIEW_SHARE_ORIGIN ?? DEFAULT_STORE_ORIGIN);
2594
- } catch {
2595
- throw new ReviewInputError("DEV_REVIEW_SHARE_ORIGIN must be a bare HTTPS origin.");
2596
- }
2597
- if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw new ReviewInputError("DEV_REVIEW_SHARE_ORIGIN must be a bare HTTPS origin.");
2598
- return {
2599
- origin: url.origin,
2600
- token
2601
- };
2602
- }
2603
- //#endregion
2604
- //#region src/slug.ts
2605
- /** Lowercase ASCII slug: runs of anything but `a-z0-9` become one `-`, outer
2606
- * dashes go. Empty input, or input with no letters or digits, yields `""`;
2607
- * callers choose their own fallback (`"section"`, `"diagram"`, `"database"`).
2608
- * This is the rule sequence and lens ids were already published with, so it
2609
- * must not change. */
2610
- function slugify(text) {
2611
- return text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
2612
- }
2613
- //#endregion
2614
- //#region src/sharing/file.ts
2615
- const WHITEBOARD_FILE_EXTENSION = ".whiteboard";
2616
- const headerSchema = z.looseObject({ format: z.string() });
2617
- function whiteboardFileName(title) {
2618
- return `${slugify(title) || "untitled"}${WHITEBOARD_FILE_EXTENSION}`;
2619
- }
2620
- function whiteboardFileText(bundle) {
2621
- return JSON.stringify(bundle, null, 2) + "\n";
2622
- }
2623
- function parseWhiteboardFile(text) {
2624
- let parsed;
2625
- try {
2626
- parsed = JSON.parse(text);
2627
- } catch {
2628
- parsed = void 0;
2629
- }
2630
- const header = headerSchema.safeParse(parsed);
2631
- if (!header.success) throw new ReviewInputError("This is not a Whiteboard file.", 400);
2632
- if (header.data.format !== "review-share/2") throw new ReviewInputError("This file needs a different Whiteboard version.", 400);
2633
- return shareBundleSchema.parse(parsed);
2634
- }
2635
- function whiteboardFileKey(text) {
2636
- return createHash("sha256").update("whiteboard-file\0").update(text).digest("hex");
2637
- }
2638
- //#endregion
2639
- //#region src/sharing/host.ts
2640
- const publishSchema = z.strictObject({
2641
- reviewId: z.string().min(1),
2642
- version: z.number().int().nonnegative().optional(),
2643
- requestId: z.uuid().optional()
2644
- });
2645
- const importSchema = z.union([z.strictObject({ url: z.string().max(2048) }), z.strictObject({ path: z.string().min(1).max(4096) })]);
2646
- /** Mounted behind local host authentication. Account credentials never enter the renderer. */
2647
- function mountSharingHost(app, store, data, options = {}) {
2648
- const startLogin = options.login ?? runStoreLogin;
2649
- let login = { pending: false };
2650
- let warmedAt = 0;
2651
- const readAccount = async () => {
2652
- const account = await readStoreAuth();
2653
- if (account && Date.now() - warmedAt > 6e4) {
2654
- warmedAt = Date.now();
2655
- new ShareClient(account.origin, account.token, options.fetch).warm().catch(() => {});
2656
- }
2657
- return {
2658
- account: account ? {
2659
- login: account.login,
2660
- origin: account.origin,
2661
- email: account.email,
2662
- githubLogin: account.githubLogin
2663
- } : null,
2664
- ...login
2665
- };
2666
- };
2667
- app.get("/sharing/account", async (context) => context.json(await readAccount()));
2668
- app.post("/sharing/logout", async (context) => {
2669
- await clearStoreAuth();
2670
- return context.json(await readAccount());
2671
- });
2672
- app.post("/sharing/login", async (context) => {
2673
- if (!login.pending) {
2674
- login = { pending: true };
2675
- options.onLogin?.("started");
2676
- const discard = new Writable({ write(_chunk, _encoding, done) {
2677
- done();
2678
- } });
2679
- startLogin({
2680
- traces: false,
2681
- stdout: discard,
2682
- stderr: discard,
2683
- openUrl: options.openUrl,
2684
- onDeviceCode: (device) => {
2685
- Object.assign(login, device);
2686
- }
2687
- }).then((code) => {
2688
- login = { pending: false };
2689
- if (code) login.error = "Sign-in did not finish. Try again.";
2690
- options.onLogin?.(code ? "failed" : "succeeded", code ? "did_not_finish" : void 0);
2691
- }).catch(() => {
2692
- login = {
2693
- pending: false,
2694
- error: "Sign-in failed. Try again."
2695
- };
2696
- options.onLogin?.("failed", "error");
2697
- });
2698
- }
2699
- return context.json(login);
2700
- });
2701
- mountSharingPublisher(app, store, data, options);
2702
- const imports = /* @__PURE__ */ new Map();
2703
- let importing = Promise.resolve();
2704
- app.get("/sharing/import/:id", (context) => {
2705
- const id = context.req.param("id");
2706
- const { reviewId = id, ...status } = imports.get(id) ?? {
2707
- stage: "error",
2708
- error: "Shared review is not available."
2709
- };
2710
- return context.json({
2711
- reviewId,
2712
- ...status,
2713
- ...status.stage === "ready" && { title: store.read(reviewId).title }
2714
- });
2715
- });
2716
- const startImport = (id, initial, load) => {
2717
- const known = imports.get(id)?.stage;
2718
- if (!known || known === "ready" || known === "error") {
2719
- const copy = store.copyOf(id);
2720
- const status = copy ? {
2721
- stage: "ready",
2722
- reviewId: copy
2723
- } : { stage: initial };
2724
- imports.set(id, status);
2725
- if (!copy) importing = importing.then(async () => {
2726
- const { bundle, attribution } = await load();
2727
- status.reviewId = await importShare({
2728
- store,
2729
- data,
2730
- fetchRepository: options.fetchRepository,
2731
- cloneRepository: options.cloneRepository,
2732
- onStage: (stage) => {
2733
- status.stage = stage;
2734
- }
2735
- }, id, bundle, attribution);
2736
- status.stage = "ready";
2737
- }).catch((error) => {
2738
- status.stage = "error";
2739
- status.error = error instanceof ReviewInputError ? error.message : "This share is unavailable, revoked, or needs a newer Whiteboard version.";
2740
- });
2741
- }
2742
- const { reviewId: _reviewId, ...status } = imports.get(id);
2743
- return {
2744
- reviewId: id,
2745
- ...status
2746
- };
2747
- };
2748
- app.post("/sharing/import", async (context) => {
2749
- const input = importSchema.parse(await readBoundedRequestJson(context.req.raw));
2750
- if ("path" in input) {
2751
- if (!path.isAbsolute(input.path)) throw new ReviewInputError("Use an absolute file path.", 400);
2752
- const text = await readFile(input.path, "utf8").catch(() => {
2753
- throw new ReviewInputError(`Could not read ${path.basename(input.path)}. Check that the file still exists and Whiteboard can open it.`, 400);
2754
- });
2755
- return context.json(startImport(whiteboardFileKey(text), "reading", async () => ({ bundle: parseWhiteboardFile(text) })), 202);
2756
- }
2757
- const parsed = parseShareLink(input.url);
2758
- const account = await readStoreAuth();
2759
- if (parsed.origin !== DEFAULT_STORE_ORIGIN && parsed.origin !== account?.origin) throw new ReviewInputError("This share uses an untrusted service. Sign in to that service before opening its links.", 400);
2760
- return context.json(startImport(shareKey(parsed.origin, parsed.capability), "downloading", () => new ShareClient(parsed.origin).download(parsed.capability)), 202);
2761
- });
2762
- }
2763
- /** Publishing needs only an authored store, never Desktop or recipient workspaces. */
2764
- function mountSharingPublisher(app, store, data, options = {}) {
2765
- const shareable = (input) => {
2766
- const snapshot = store.read(input.reviewId, input.version);
2767
- if (snapshot.target?.kind === "worktree") throw new ReviewInputError("Pin this review to commits before sharing it.");
2768
- validateShareSources(snapshot);
2769
- return snapshot;
2770
- };
2771
- const shareBundle = async (snapshot) => {
2772
- const repositoryId = snapshot.pins?.repositoryId ?? snapshot.target?.repositoryId;
2773
- const root = repositoryId ? store.repositoryPath(repositoryId) : void 0;
2774
- const repository = root ? (options.readRepository ?? readShareRepository)(root).catch(() => void 0) : void 0;
2775
- const bundle = await exportShare({
2776
- store,
2777
- data,
2778
- reviewId: snapshot.reviewId,
2779
- version: snapshot.version
2780
- });
2781
- const verified = await repository;
2782
- if (verified) bundle.repository = verified;
2783
- return bundle;
2784
- };
2785
- app.post("/sharing/export", async (context) => {
2786
- const snapshot = shareable(publishSchema.parse(await readBoundedRequestJson(context.req.raw)));
2787
- const bundle = await shareBundle(snapshot);
2788
- if (!bundle.repository && needsRepository(bundle, snapshot)) throw new ReviewInputError("Exporting pinned code needs a GitHub remote the file's reader can fetch from.");
2789
- return context.json({
2790
- filename: whiteboardFileName(snapshot.title),
2791
- text: whiteboardFileText(bundle)
2792
- });
2793
- });
2794
- app.post("/sharing/publish", async (context) => {
2795
- const input = publishSchema.parse(await readBoundedRequestJson(context.req.raw));
2796
- const snapshot = shareable(input);
2797
- const account = await readSharingAuth();
2798
- if (!account) throw new ReviewInputError("Set DEV_REVIEW_SHARE_TOKEN for CI, or run whiteboard login before sharing.", 409);
2799
- const bundle = await shareBundle(snapshot);
2800
- try {
2801
- const result = await new ShareClient(account.origin, account.token, options.fetch).create(bundle, input.requestId ?? randomUUID());
2802
- const url = new URL(result.url);
2803
- if (process.env.DEV_FAST_REVIEW_APP_URL_PROTOCOL === "dev-fast-review-preview") url.searchParams.set("app", "preview");
2804
- options.onPublished?.({
2805
- reviewId: input.reviewId,
2806
- version: snapshot.version
2807
- });
2808
- return context.json({
2809
- ...result,
2810
- url: url.href,
2811
- version: snapshot.version
2812
- });
2813
- } catch (error) {
2814
- if (error instanceof ReviewInputError) throw error;
2815
- if (error instanceof ShareAuthError) {
2816
- if (process.env.DEV_REVIEW_SHARE_TOKEN === void 0) await clearStoreAuth();
2817
- throw new ReviewInputError("Your sign-in has expired. Sign in again to share.", 401);
2818
- }
2819
- throw new ReviewInputError("Sharing failed. Check your connection and login, then retry.", 409);
2820
- }
2821
- });
2822
- app.post("/sharing/revoke", async (context) => {
2823
- const { shareId } = z.strictObject({ shareId: shareIdSchema }).parse(await readBoundedRequestJson(context.req.raw));
2824
- const account = await readSharingAuth();
2825
- if (!account) throw new ReviewInputError("Set DEV_REVIEW_SHARE_TOKEN for CI, or run whiteboard login first.", 409);
2826
- await new ShareClient(account.origin, account.token).revoke(shareId);
2827
- options.onRevoked?.({ shareId });
2828
- return context.json({
2829
- shareId,
2830
- revoked: true
2831
- });
2832
- });
2833
- }
2834
- //#endregion
2835
2917
  //#region src/review-api/anchor-quotes.ts
2836
2918
  const QUOTE_LIMIT = 20;
2837
2919
  const clip = (line) => line.trim() === "" ? "(blank line)" : line.length > 120 ? `${line.slice(0, 119)}…` : line;
@@ -2971,12 +3053,21 @@ function documentText(snapshot, targetId, full = false, relations, runs) {
2971
3053
  for (const edge of element.edges) detail(`[${edge.id}] ${edge.from} → ${edge.to}${edge.label ? `: ${edge.label}` : ""}`);
2972
3054
  break;
2973
3055
  case "animation":
2974
- for (const [index, frame] of element.frames.entries()) {
2975
- detail(`Frame ${index + 1} [${frame.key}]: ${frame.title}${frame.caption ? ` — ${frame.caption}` : ""}`);
2976
- if (frame.code) write(depth + 2, `Code: ${frame.code.join(", ")}`);
2977
- if (detailed && frame.data !== void 0) write(depth + 2, `Data: ${JSON.stringify(frame.data)}`);
3056
+ case "clip":
3057
+ if (element.type === "animation") {
3058
+ for (const [index, frame] of element.frames.entries()) {
3059
+ detail(`Frame ${index + 1} [${frame.key}]: ${frame.title}${frame.caption ? ` — ${frame.caption}` : ""}`);
3060
+ if (frame.code) write(depth + 2, `Code: ${frame.code.join(", ")}`);
3061
+ if (detailed && frame.data !== void 0) write(depth + 2, `Data: ${JSON.stringify(frame.data)}`);
3062
+ }
3063
+ for (const scenario of element.scenarios ?? []) detail(`Scenario [${scenario.key}] ${scenario.label}: ${scenario.frames.join(" → ")}`);
3064
+ } else {
3065
+ for (const [index, marker] of element.markers.entries()) {
3066
+ detail(`Marker ${index + 1} [${marker.key}]: ${marker.title}${marker.caption ? ` — ${marker.caption}` : ""}`);
3067
+ if (marker.code) write(depth + 2, `Code: ${marker.code.join(", ")}`);
3068
+ }
3069
+ for (const scenario of element.scenarios ?? []) detail(`Scenario [${scenario.key}] ${scenario.label}`);
2978
3070
  }
2979
- for (const scenario of element.scenarios ?? []) detail(`Scenario [${scenario.key}] ${scenario.label}: ${scenario.frames.join(" → ")}`);
2980
3071
  if (runs) {
2981
3072
  const run = element.id === void 0 ? void 0 : runs.get(element.id);
2982
3073
  write(depth + 1, run?.error ? `Script error: ${run.error}` : run ? "Ran in an open whiteboard without a script error." : "Not run yet: its script runs only where the whiteboard is open.");
@@ -3553,7 +3644,7 @@ const remoteCaller = (context) => context.req.header(REVIEW_CLIENT_HEADER) === R
3553
3644
  function createReviewApi(store, data, open, sharing = false, capabilities = () => ({
3554
3645
  desktopAvailable: Boolean(open),
3555
3646
  softwareMapEnabled: false
3556
- }), traceEnabled = async () => false, status = () => ({}), hooks = {}, ask, animationsEnabled = () => false) {
3647
+ }), traceEnabled = async () => false, status = () => ({}), hooks = {}, ask, animationsEnabled = () => false, render) {
3557
3648
  const app = new Hono();
3558
3649
  app.onError((error, context) => {
3559
3650
  if (error instanceof HttpJsonError) return context.json({ error: error.message }, error.statusCode);
@@ -4561,6 +4652,23 @@ function createReviewApi(store, data, open, sharing = false, capabilities = () =
4561
4652
  ...listedRelations(store.relations(id))
4562
4653
  });
4563
4654
  });
4655
+ app.get("/:id/render", async (context) => {
4656
+ const query = renderQuerySchema.parse(context.req.query());
4657
+ const id = context.req.param("id");
4658
+ const target = renderTarget(readReview(id), query.targetId, query.frame);
4659
+ if (!render) throw new ReviewInputError("Rendering a frame needs Whiteboard Desktop, which this server cannot reach.", 409);
4660
+ const rendered = await render({
4661
+ reviewId: id,
4662
+ targetId: query.targetId,
4663
+ frame: target.key
4664
+ });
4665
+ return context.json({
4666
+ reviewId: id,
4667
+ targetId: query.targetId,
4668
+ type: target.type,
4669
+ ...rendered
4670
+ });
4671
+ });
4564
4672
  app.get("/:id", async (context) => {
4565
4673
  const query = readQuerySchemas.get.parse(context.req.query());
4566
4674
  let snapshot = { ...readReview(context.req.param("id"), query.version) };
@@ -4594,7 +4702,8 @@ function createReviewApi(store, data, open, sharing = false, capabilities = () =
4594
4702
  const { edit, reviewId } = input.operation;
4595
4703
  const incoming = "content" in edit ? elements([edit.content]) : [];
4596
4704
  const target = "targetId" in edit ? elements(readReview(reviewId).document).find((element) => element.id === edit.targetId) : void 0;
4597
- if (incoming.some((element) => element.type === "animation") || target?.type === "animation") throw new ReviewInputError("Animation authoring is disabled. Enable Animations in Settings under Experimental Features to edit animations. Existing animations can still be viewed.", 409);
4705
+ const animated = (type) => type === "animation" || type === "clip";
4706
+ if (incoming.some((element) => animated(element.type)) || animated(target?.type)) throw new ReviewInputError("Animation authoring is disabled. Enable Animations in Settings under Experimental Features to edit animations and clips. Existing ones can still be viewed.", 409);
4598
4707
  }
4599
4708
  const threadId = remoteCaller(context) ? void 0 : context.req.header("x-review-thread")?.trim() || void 0;
4600
4709
  const author = !remoteCaller(context) && context.req.header("x-review-person") !== void 0 ? { kind: "person" } : {
@@ -4605,7 +4714,7 @@ function createReviewApi(store, data, open, sharing = false, capabilities = () =
4605
4714
  const result = await store.execute(input, void 0, author);
4606
4715
  const key = copy?.shared?.key;
4607
4716
  const kept = copy && boardRepositoryId(copy);
4608
- if (result.deleted && key && kept) await Promise.resolve().then(() => forgetSharedCommits(store.repositoryPath(kept), key)).catch((error) => console.error("Could not drop a copy's kept commits:", error));
4717
+ if (result.deleted && key && kept && !store.copiesOf(key).some(({ reviewId }) => boardRepositoryId(store.read(reviewId)) === kept)) await Promise.resolve().then(() => forgetSharedCommits(store.repositoryPath(kept), key)).catch((error) => console.error("Could not drop a copy's kept commits:", error));
4609
4718
  if (input.operation.type === "edit" && data && !result.deleted) {
4610
4719
  const { quotes, unquoted } = await anchorQuotes(result.version > 0 ? store.read(result.reviewId, result.version - 1) : void 0, store.read(result.reviewId, result.version), async (pins, side, file) => (await data.file(pins, side, file)).text);
4611
4720
  return context.json({
@@ -4902,7 +5011,7 @@ function createReviewServerApp(input) {
4902
5011
  app.get("/control", (context) => openControlEvents(context, input.relay));
4903
5012
  mountFeedback(app);
4904
5013
  app.post("/control/result", async (context) => {
4905
- const accepted = input.relay.acceptResult(await readBoundedRequestJson(context.req.raw));
5014
+ const accepted = input.relay.acceptResult(await readBoundedRequestJson(context.req.raw, VERB_RESULT_MAX_BYTES));
4906
5015
  return serverJson(accepted ? 200 : 404, { ok: accepted });
4907
5016
  });
4908
5017
  answerErrorsAsJson(app);
@@ -4926,6 +5035,9 @@ function answerErrorsAsJson(app) {
4926
5035
  });
4927
5036
  });
4928
5037
  }
5038
+ /** A verb result may carry a captured frame: a PNG of up to 1600 pixels on
5039
+ * its longest edge, base64. */
5040
+ const VERB_RESULT_MAX_BYTES = 16777216;
4929
5041
  function createWhiteboardCore(input) {
4930
5042
  const { store, data } = input.profile;
4931
5043
  const app = createReviewServerApp({
@@ -4934,7 +5046,7 @@ function createWhiteboardCore(input) {
4934
5046
  serverId: store.serverId(),
4935
5047
  relay: input.relay
4936
5048
  });
4937
- const callbacks = relayReviewCallbacks(input.relay, input.softwareMapEnabled);
5049
+ const callbacks = relayReviewCallbacks(input.relay, input.softwareMapEnabled, input.animations);
4938
5050
  const { ask } = input;
4939
5051
  const launch = ask?.launch ?? launchAskAgent;
4940
5052
  const askThreads = ask && new AskThreads(async (agent, cwd, options) => launch(agent, cwd, {
@@ -4951,21 +5063,54 @@ function createWhiteboardCore(input) {
4951
5063
  const api = createReviewApi(store, data, callbacks.open, input.sharing, callbacks.capabilities, () => traceMachineEnabled(), input.status, input.hooks, askThreads && {
4952
5064
  threads: askThreads,
4953
5065
  agents: async () => detectAskAgents(await ask?.env)
4954
- }, input.animations);
5066
+ }, callbacks.animations, callbacks.render);
4955
5067
  if (!input.sharing) mountSharingPublisher(api, store, data);
4956
5068
  return {
4957
5069
  app,
4958
5070
  api,
4959
5071
  close: () => {
5072
+ callbacks.close();
4960
5073
  stopLocal?.();
4961
5074
  closeDeletedThreads();
4962
5075
  askThreads?.closeAll();
4963
5076
  }
4964
5077
  };
4965
5078
  }
4966
- /** The Desktop callbacks `createReviewApi` takes, answered over the relay. */
4967
- function relayReviewCallbacks(relay, softwareMapEnabled = false) {
5079
+ const capabilitiesAnswerSchema = z.object({
5080
+ softwareMapEnabled: z.boolean(),
5081
+ animationsEnabled: z.boolean().optional()
5082
+ });
5083
+ /**
5084
+ * The Desktop callbacks `createReviewApi` takes, answered over the relay.
5085
+ * `animations` never asks Desktop: it is the last answer while one is
5086
+ * attached, else the host's own.
5087
+ */
5088
+ function relayReviewCallbacks(relay, softwareMapEnabled = false, animations = () => false) {
5089
+ let desktopAnimations;
5090
+ const capabilities = async () => {
5091
+ if (!relay.attached) return {
5092
+ desktopAvailable: false,
5093
+ softwareMapEnabled
5094
+ };
5095
+ const result = await relay.dispatch({
5096
+ name: "authoringCapabilities",
5097
+ args: {}
5098
+ });
5099
+ if (!result.ok) throw new ReviewInputError(result.error, 409);
5100
+ const answer = capabilitiesAnswerSchema.parse(result.result);
5101
+ desktopAnimations = answer.animationsEnabled;
5102
+ return {
5103
+ desktopAvailable: true,
5104
+ softwareMapEnabled: answer.softwareMapEnabled
5105
+ };
5106
+ };
4968
5107
  return {
5108
+ capabilities,
5109
+ animations: () => desktopAnimations ?? animations(),
5110
+ close: relay.onAttachedChange(() => {
5111
+ desktopAnimations = void 0;
5112
+ if (relay.attached) capabilities().catch(() => {});
5113
+ }),
4969
5114
  async open(review) {
4970
5115
  const result = await relay.dispatch({
4971
5116
  name: "openApiReview",
@@ -4974,20 +5119,19 @@ function relayReviewCallbacks(relay, softwareMapEnabled = false) {
4974
5119
  if (!result.ok) throw new ReviewInputError(result.error, 409);
4975
5120
  return z.object({ softwareMapEnabled: z.boolean() }).parse(result.result);
4976
5121
  },
4977
- async capabilities() {
4978
- if (!relay.attached) return {
4979
- desktopAvailable: false,
4980
- softwareMapEnabled
4981
- };
5122
+ async render(request) {
5123
+ if (!relay.attached) throw new ReviewInputError("No Whiteboard Desktop is attached, so nothing can show the animation. Open the whiteboard in Desktop and render again.", 409);
5124
+ const { reviewId, targetId, frame } = request;
4982
5125
  const result = await relay.dispatch({
4983
- name: "authoringCapabilities",
4984
- args: {}
5126
+ name: "renderAnimation",
5127
+ args: {
5128
+ reviewId,
5129
+ targetId,
5130
+ frame
5131
+ }
4985
5132
  });
4986
5133
  if (!result.ok) throw new ReviewInputError(result.error, 409);
4987
- return {
4988
- desktopAvailable: true,
4989
- ...z.object({ softwareMapEnabled: z.boolean() }).parse(result.result)
4990
- };
5134
+ return renderedFrameSchema.parse(result.result);
4991
5135
  }
4992
5136
  };
4993
5137
  }
@@ -5220,6 +5364,7 @@ async function serve(input) {
5220
5364
  await migrateDiffrConfig(input.signal);
5221
5365
  if (input.telemetry) await drainServerCrashReport(input.telemetry);
5222
5366
  const local = await openReviewProfile(input.stateDir, { manageWorkspaces: true });
5367
+ const animationsEnabled = await readAnimationsEnabled(input.stateDir);
5223
5368
  const discovery = {
5224
5369
  version: 1,
5225
5370
  instanceId: randomUUID(),
@@ -5236,6 +5381,7 @@ async function serve(input) {
5236
5381
  token: discovery.token,
5237
5382
  instanceId: discovery.instanceId,
5238
5383
  softwareMapEnabled: input.softwareMapEnabled,
5384
+ animations: () => animationsEnabled,
5239
5385
  status: () => ({
5240
5386
  key: "headless",
5241
5387
  home: input.stateDir
@@ -5285,4 +5431,4 @@ async function serve(input) {
5285
5431
  }
5286
5432
  }
5287
5433
  //#endregion
5288
- export { setDiffrConfigValue as C, saveDiffrSummarizer as S, cliAskTools as T, HttpJsonError as _, drainServerCrashReport as a, migrateDiffrConfig as b, answerErrorsAsJson as c, captureSanitizedUiTelemetry as d, clientErrorsForSession as f, DEFAULT_MAX_REQUEST_BYTES as g, readBoundedRequestJson as h, withHeadlessServerLock as i, createWhiteboardCore as l, createNodeRequestListener as m, runHeadlessServer as n, installProcessErrorTelemetry as o, recordClientError as p, takeOverServerHome as r, serverCrashReportPath as s, HeadlessServerBusyError as t, serverJson as u, ReviewServerError as v, testDiffrSummarizer as w, readDiffrConfig as x, GlobalReviewDesktopVerbRelay as y };
5434
+ export { DEFAULT_MAX_REQUEST_BYTES as C, writeAnimationsEnabled as D, readAnimationsEnabled as E, cliAskTools as O, readBoundedRequestJson as S, ReviewServerError as T, saveDiffrSummarizer as _, drainServerCrashReport as a, remoteShare as b, answerErrorsAsJson as c, captureSanitizedUiTelemetry as d, clientErrorsForSession as f, readDiffrConfig as g, migrateDiffrConfig as h, withHeadlessServerLock as i, createWhiteboardCore as l, GlobalReviewDesktopVerbRelay as m, runHeadlessServer as n, installProcessErrorTelemetry as o, recordClientError as p, takeOverServerHome as r, serverCrashReportPath as s, HeadlessServerBusyError as t, serverJson as u, setDiffrConfigValue as v, HttpJsonError as w, createNodeRequestListener as x, testDiffrSummarizer as y };