@dev.fast/whiteboard 0.5.1 → 0.5.2-preview.20261104.146

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +4 -4
  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,2 +1,2 @@
1
- import { i as withHeadlessServerLock, n as runHeadlessServer, r as takeOverServerHome, t as HeadlessServerBusyError } from "./headless-host-Dl2KZ9IT.js";
1
+ import { i as withHeadlessServerLock, n as runHeadlessServer, r as takeOverServerHome, t as HeadlessServerBusyError } from "./headless-host-FAlbekFD.js";
2
2
  export { HeadlessServerBusyError, runHeadlessServer, takeOverServerHome, withHeadlessServerLock };
@@ -1,8 +1,8 @@
1
1
  import { f as gitAt } from "./src-Y30gweiK.js";
2
2
  import { u as parseJsonText } from "./src-X9phtB2j.js";
3
- import { $ as comparisonKey, A as parseMarkdown, B as lensSchema, E as sourceReferences, L as UNCATEGORIZED_LENS_ID, M as checkReferences, O as markdownNodes, Q as anchorSelection, T as selectionReferences, V as lensSelections, Z as SelectionUnavailableError, _ as readsAtBoardPins, at as unionIntervals, c as documentSchema, h as pinsSchema, it as sourceAnchors, n as anchorPins, nt as resolveDiffSelection, o as boardRepositoryId, ot as ReviewInputError, q as fileLineRangeSchema, rt as selectionKey, u as elements, v as resourceReference, y as resourceReferences } from "./src-B2b8cCoI.js";
4
- import { s as safeStorageSegment } from "./review-home-paths-LdGS4SpR.js";
5
- import { D as scopedCoverage, O as sourcesForFile, T as emptyCoverage, b as textIncludesQuote, c as traceSchema, k as subtractIntervals, n as migrateStoredDocument, r as migrateStoredLenses, s as softwareModelDataSchema, t as liftFileLenses, u as pullRequestUrl, w as coverageSources, x as textualRows } from "./stored-document-migration-iwIQ4l5G.js";
3
+ import { $ as comparisonKey, A as parseMarkdown, B as lensSchema, E as sourceReferences, L as UNCATEGORIZED_LENS_ID, M as checkReferences, O as markdownNodes, Q as anchorSelection, T as selectionReferences, V as lensSelections, Z as SelectionUnavailableError, _ as readsAtBoardPins, at as unionIntervals, c as documentSchema, h as pinsSchema, it as sourceAnchors, n as anchorPins, nt as resolveDiffSelection, o as boardRepositoryId, ot as ReviewInputError, q as fileLineRangeSchema, rt as selectionKey, u as elements, v as resourceReference, y as resourceReferences } from "./src-BQDlJXkG.js";
4
+ import { s as safeStorageSegment } from "./review-home-paths-BmZX2qiz.js";
5
+ import { D as scopedCoverage, O as sourcesForFile, T as emptyCoverage, b as textIncludesQuote, c as traceSchema, k as subtractIntervals, n as migrateStoredDocument, r as migrateStoredLenses, s as softwareModelDataSchema, t as liftFileLenses, u as pullRequestUrl, w as coverageSources, x as textualRows } from "./stored-document-migration-D1wK4yID.js";
6
6
  import { N as devReviewHome } from "./s3-config-CjcLyFV5.js";
7
7
  import { createHash, randomUUID } from "node:crypto";
8
8
  import { z } from "zod";
@@ -36,51 +36,93 @@ const gitHubRepositoryUrlSchema = z.string().refine((value) => {
36
36
  const commitSchema = z.string().regex(/^[a-f0-9]{40}([a-f0-9]{24})?$/);
37
37
  const sideSchema = z.enum(["base", "head"]);
38
38
  /** An excerpt is the lines a board references. */
39
- const shareSourceSchema = z.discriminatedUnion("kind", [z.strictObject({
39
+ const shareSourceSchema = z.discriminatedUnion("kind", [z.object({
40
40
  kind: z.literal("excerpt"),
41
41
  file: sharePathSchema,
42
42
  commit: commitSchema,
43
43
  fromLine: z.number().int().positive(),
44
44
  lines: z.array(z.string()).min(1)
45
45
  })]);
46
- const lineRangeSchema = z.strictObject({
46
+ const lineRangeSchema = z.object({
47
47
  side: sideSchema,
48
48
  file: sharePathSchema,
49
49
  fromLine: z.number().int().positive(),
50
50
  toLine: z.number().int().positive(),
51
- pins: z.strictObject({
51
+ pins: z.object({
52
52
  repositoryId: z.string(),
53
53
  head: z.string(),
54
54
  base: z.string().optional()
55
55
  }).optional()
56
56
  });
57
- const shareBundleSchema = z.strictObject({
57
+ /** 1-based base and head lines; null when a side lacks the line. */
58
+ const alignmentRowSchema = z.tuple([z.number().int().positive().nullable(), z.number().int().positive().nullable()]);
59
+ const shareAlignmentSchema = z.object({
60
+ file: sharePathSchema,
61
+ previousFile: sharePathSchema.optional(),
62
+ head: z.string(),
63
+ base: z.string().optional(),
64
+ rows: z.array(alignmentRowSchema).min(1)
65
+ });
66
+ /** Unknown fields drop, so a reader opens bundles from newer sharers. */
67
+ const shareBundleSchema = z.object({
58
68
  format: z.literal(SHARE_FORMAT),
59
69
  reviewId: z.string().min(1).max(256),
60
70
  version: z.number().int().nonnegative(),
61
71
  title: z.string().max(4096),
62
72
  /** Validated against the document schema by the reader. */
63
73
  snapshot: z.record(z.string(), z.unknown()),
64
- images: z.record(z.string(), z.strictObject({ png: z.base64() })),
74
+ images: z.record(z.string(), z.object({ png: z.base64() })),
65
75
  traces: z.record(z.string(), z.unknown()),
66
76
  maps: z.record(z.string(), z.unknown()),
67
77
  commits: z.array(z.unknown()),
68
78
  /** Each reference's lines, resolved where the board was shared. */
69
79
  selections: z.record(z.string(), z.array(lineRangeSchema)),
70
80
  sources: z.array(shareSourceSchema),
71
- repository: z.strictObject({ cloneUrl: gitHubRepositoryUrlSchema }).optional()
81
+ /** The sharer's diff over the selections, since 0.4. */
82
+ alignments: z.array(shareAlignmentSchema).optional(),
83
+ repository: z.object({ cloneUrl: gitHubRepositoryUrlSchema }).optional(),
84
+ /** Quotes uncommitted changes: view only, never imported. Since 0.6. */
85
+ frozen: z.literal(true).optional()
72
86
  });
73
- z.strictObject({
87
+ z.object({
74
88
  requestId: z.uuid(),
75
89
  bundle: shareBundleSchema
76
90
  });
91
+ const shareCreatedSchema = z.strictObject({
92
+ shareId: shareIdSchema,
93
+ url: z.url()
94
+ });
95
+ const shareSizeSchema = z.number().int().positive().max(MAX_SHARE_BYTES);
96
+ const shareRevisionRefSchema = z.object({
97
+ id: shareIdSchema,
98
+ version: z.number().int().nonnegative(),
99
+ createdAt: z.number()
100
+ });
101
+ const shareRevisionSchema = shareRevisionRefSchema.extend({
102
+ size: shareSizeSchema,
103
+ url: z.url()
104
+ });
105
+ const shareRevisionsSchema = z.object({
106
+ latest: shareIdSchema,
107
+ items: z.array(shareRevisionSchema)
108
+ });
109
+ const sharedMetaSchema = z.object({
110
+ sender: z.strictObject({ login: z.string().min(1).max(256) }),
111
+ sharedAt: z.number(),
112
+ size: shareSizeSchema,
113
+ revision: shareRevisionRefSchema,
114
+ latest: shareIdSchema
115
+ });
77
116
  function parseShareLink(value) {
78
117
  const url = new URL(value);
79
- if (url.protocol !== "https:" || url.username || url.password || url.hash || [...url.searchParams.keys()].some((key) => key !== "app")) throw new Error("Use an HTTPS Whiteboard share link.");
80
- return {
118
+ if (url.protocol !== "https:" || url.username || url.password || url.hash || [...url.searchParams.keys()].some((key) => key !== "app" && key !== "rev")) throw new Error("Use an HTTPS Whiteboard share link.");
119
+ const link = {
81
120
  origin: url.origin,
82
121
  capability: capabilitySchema.parse(/^\/s\/([^/]+)$/.exec(url.pathname)?.[1])
83
122
  };
123
+ const revision = url.searchParams.get("rev");
124
+ if (revision !== null) link.revision = shareIdSchema.parse(revision);
125
+ return link;
84
126
  }
85
127
  //#endregion
86
128
  //#region src/server/bounded-stream.ts
@@ -425,7 +467,11 @@ async function exportShare(input) {
425
467
  const stored = structuredClone(input.store.read(input.reviewId, input.version));
426
468
  const repositoryId = boardRepositoryId(stored);
427
469
  const { target, staleSources: _staleSources, sourceUnavailable: _sourceUnavailable, lastEdit: _lastEdit, trashedAt: _trashedAt, personEdit: _personEdit, ...snapshot } = stored;
428
- if (target?.kind === "worktree") throw new ReviewInputError("Pin this review to commits before sharing it.");
470
+ const live = target?.kind === "worktree" ? await input.data.sourcePins(stored) : void 0;
471
+ if (live) {
472
+ const { worktreeRevision: _worktreeRevision, ...committed } = live;
473
+ snapshot.pins = committed;
474
+ }
429
475
  const pullRequestUrl = snapshot.origin?.pullRequestUrl;
430
476
  const baseBranch = pullRequestUrl && !snapshot.origin?.baseBranch && (snapshot.origin?.pullRequestBaseBranch ?? input.data.pullRequestBaseBranch(pullRequestUrl));
431
477
  if (baseBranch) snapshot.origin = {
@@ -439,7 +485,7 @@ async function exportShare(input) {
439
485
  if (block.type !== "markdown") continue;
440
486
  for (const node of markdownNodes(parseMarkdown(block.markdown))) if (node.type === "image") throw new ReviewInputError("Convert Markdown images to managed image blocks before sharing.");
441
487
  }
442
- const pins = snapshot.pins;
488
+ const pins = live ?? snapshot.pins;
443
489
  const references = [...sourceReferences(snapshot.document, { repositoryId }), ...lensSelections(snapshot.lenses ?? []).flatMap((selection) => sourceAnchors(selection.source).map((source) => ({ source })))];
444
490
  const traces = /* @__PURE__ */ new Map();
445
491
  const maps = /* @__PURE__ */ new Map();
@@ -480,7 +526,16 @@ async function exportShare(input) {
480
526
  fromLine,
481
527
  toLine
482
528
  }));
483
- return {
529
+ let frozen = false;
530
+ const sources = await cutExcerpts({ async quote(at, range) {
531
+ const quote = await input.data.quote(at, range);
532
+ if (at.worktreeRevision && range.side === "head" && !frozen) {
533
+ const { worktreeRevision: _worktreeRevision, ...committed } = at;
534
+ if (await input.data.quote(committed, range).then((head) => head.text !== quote.text, () => true)) frozen = true;
535
+ }
536
+ return quote;
537
+ } }, excerpts);
538
+ const bundle = {
484
539
  format: SHARE_FORMAT,
485
540
  reviewId: snapshot.reviewId,
486
541
  version: snapshot.version,
@@ -491,9 +546,49 @@ async function exportShare(input) {
491
546
  maps: Object.fromEntries(maps),
492
547
  commits: pins ? await input.data.commits((await input.data.resolveSource(stored)).pins) : [],
493
548
  selections,
494
- sources: await cutExcerpts(input.data, excerpts),
549
+ sources,
495
550
  ...input.repository && { repository: input.repository }
496
551
  };
552
+ if (frozen) bundle.frozen = true;
553
+ return bundle;
554
+ }
555
+ //#endregion
556
+ //#region src/slug.ts
557
+ /** Lowercase ASCII slug: runs of anything but `a-z0-9` become one `-`, outer
558
+ * dashes go. Empty input, or input with no letters or digits, yields `""`;
559
+ * callers choose their own fallback (`"section"`, `"diagram"`, `"database"`).
560
+ * This is the rule sequence and lens ids were already published with, so it
561
+ * must not change. */
562
+ function slugify(text) {
563
+ return text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
564
+ }
565
+ //#endregion
566
+ //#region src/sharing/file.ts
567
+ const WHITEBOARD_FILE_EXTENSION = ".whiteboard";
568
+ const WHITEBOARD_UPGRADE_REQUIRED_MESSAGE = "This action requires a newer version of Whiteboard. Use Check for Updates, then try again.";
569
+ const headerSchema = z.looseObject({ format: z.string() });
570
+ function whiteboardFileName(title) {
571
+ return `${slugify(title) || "untitled"}${WHITEBOARD_FILE_EXTENSION}`;
572
+ }
573
+ function whiteboardFileText(bundle) {
574
+ return JSON.stringify(bundle, null, 2) + "\n";
575
+ }
576
+ function parseWhiteboardFile(text) {
577
+ let parsed;
578
+ try {
579
+ parsed = JSON.parse(text);
580
+ } catch {
581
+ parsed = void 0;
582
+ }
583
+ const header = headerSchema.safeParse(parsed);
584
+ if (!header.success) throw new ReviewInputError("This is not a Whiteboard file.", 400);
585
+ const version = /^review-share\/([1-9]\d*)$/.exec(header.data.format);
586
+ if (version && Number(version[1]) > Number("review-share/2".split("/")[1])) throw new ReviewInputError(WHITEBOARD_UPGRADE_REQUIRED_MESSAGE, 400);
587
+ if (header.data.format !== "review-share/2") throw new ReviewInputError("This file needs a different Whiteboard version.", 400);
588
+ return shareBundleSchema.parse(parsed);
589
+ }
590
+ function whiteboardFileKey(text) {
591
+ return createHash("sha256").update("whiteboard-file\0").update(text).digest("hex");
497
592
  }
498
593
  //#endregion
499
594
  //#region src/sharing/client.ts
@@ -512,15 +607,6 @@ const pooledFetch = (input, init) => {
512
607
  const jwtSchema = z.object({ token: z.string().min(1) });
513
608
  const jwtPayloadSchema = z.object({ exp: z.number() });
514
609
  const bearers = /* @__PURE__ */ new Map();
515
- const linkSchema = z.strictObject({
516
- shareId: z.uuid(),
517
- url: z.url()
518
- });
519
- const receivedSchema = z.object({
520
- sender: z.strictObject({ login: z.string().min(1).max(256) }),
521
- sharedAt: z.number(),
522
- size: z.number().int().positive().max(MAX_SHARE_BYTES)
523
- });
524
610
  /** Size is checked while reading, even if Content-Length is absent or false. */
525
611
  async function readBoundedBytes(response, limit) {
526
612
  if (!response.ok || !response.body) throw new Error(`Download failed (${response.status}).`);
@@ -543,6 +629,44 @@ var ShareAuthError = class extends Error {
543
629
  this.name = "ShareAuthError";
544
630
  }
545
631
  };
632
+ var ShareRequestError = class extends Error {
633
+ status;
634
+ constructor(status, message) {
635
+ super(message);
636
+ this.status = status;
637
+ this.name = "ShareRequestError";
638
+ }
639
+ };
640
+ const errorSchema = z.object({ error: z.object({ code: z.string() }) });
641
+ async function errorCode(response) {
642
+ if (!response.body) return void 0;
643
+ if (!response.headers.get("content-type")?.includes("application/json")) {
644
+ await response.body.cancel();
645
+ return;
646
+ }
647
+ try {
648
+ const bytes = await readBoundedStream(response.body, 16384);
649
+ return errorSchema.safeParse(parseJsonText(bytes.toString())).data?.error.code;
650
+ } catch {
651
+ return;
652
+ }
653
+ }
654
+ function requestErrorMessage(response, code) {
655
+ const { status, headers } = response;
656
+ if (status === 426 || code === "upgrade_required") return WHITEBOARD_UPGRADE_REQUIRED_MESSAGE;
657
+ if (status === 403 && headers.get("content-type")?.includes("text/html") && headers.get("server")?.toLowerCase() === "cloudflare") return "The sharing service's security filter blocked this request. Please report this issue.";
658
+ if (status === 413) return "This review is too large to share. Remove large resources, then retry.";
659
+ if (status === 429) return code === "share_quota_exceeded" ? "Your sharing storage limit has been reached. Please contact support." : "Too many sharing requests. Wait a moment, then retry.";
660
+ if (status === 400 && code === "invalid_request") return "The sharing service could not accept this review. Update Whiteboard or report this issue.";
661
+ if (status >= 500) return `The sharing service is temporarily unavailable (HTTP ${status}). Try again shortly.`;
662
+ return `The sharing service refused this request (HTTP ${status}). Please report this issue.`;
663
+ }
664
+ async function requestError(response) {
665
+ const message = requestErrorMessage(response, await errorCode(response));
666
+ const ray = response.headers.get("cf-ray");
667
+ const reference = ray && /^[a-f0-9]{16}(?:-[a-z]{3})?$/i.test(ray) ? ` Reference: ${ray}.` : "";
668
+ return new ShareRequestError(response.status, message + reference);
669
+ }
546
670
  /** Account tokens go only to the configured API origin. */
547
671
  var ShareClient = class {
548
672
  token;
@@ -570,9 +694,11 @@ var ShareClient = class {
570
694
  if (body !== void 0) init.body = body;
571
695
  const response = await this.send(this.origin + route, init);
572
696
  if (!response.ok) {
573
- await response.body?.cancel();
574
- if (response.status === 401 && !anonymous) throw new ShareAuthError();
575
- throw new Error(`Share request failed (${response.status}).`);
697
+ if (response.status === 401 && !anonymous) {
698
+ await response.body?.cancel();
699
+ throw new ShareAuthError();
700
+ }
701
+ throw await requestError(response);
576
702
  }
577
703
  return readBoundedBytes(response, maxBytes);
578
704
  }
@@ -595,13 +721,18 @@ var ShareClient = class {
595
721
  headers: { authorization: `Bearer ${token}` }
596
722
  });
597
723
  if (!response.ok) {
598
- await response.body?.cancel();
599
- if (response.status === 404 || response.status === 405) return {
600
- bearer: token,
601
- until: Date.now() + 6e5
602
- };
603
- if (response.status === 401) throw new ShareAuthError();
604
- throw new Error(`Share request failed (${response.status}).`);
724
+ if (response.status === 404 || response.status === 405) {
725
+ await response.body?.cancel();
726
+ return {
727
+ bearer: token,
728
+ until: Date.now() + 6e5
729
+ };
730
+ }
731
+ if (response.status === 401) {
732
+ await response.body?.cancel();
733
+ throw new ShareAuthError();
734
+ }
735
+ throw await requestError(response);
605
736
  }
606
737
  const { token: jwt } = jwtSchema.parse(parseJsonText(Buffer.from(await readBoundedBytes(response, 65536)).toString()));
607
738
  const { exp } = jwtPayloadSchema.parse(parseJsonText(Buffer.from(jwt.split(".")[1] ?? "", "base64url").toString()));
@@ -618,29 +749,38 @@ var ShareClient = class {
618
749
  })).body?.cancel();
619
750
  }
620
751
  async create(bundle, requestId = randomUUID()) {
621
- return linkSchema.parse(await this.api("/api/v2/shares", "POST", shareRequestBody(bundle, requestId)));
752
+ return shareCreatedSchema.parse(await this.api("/api/v2/shares", "POST", shareRequestBody(bundle, requestId)));
622
753
  }
623
754
  async list(cursor) {
624
755
  const query = cursor ? `?cursor=${z.uuid().parse(cursor)}` : "";
625
756
  return this.api(`/api/v2/shares${query}`);
626
757
  }
627
758
  async recoverLink(shareId) {
628
- return linkSchema.parse(await this.api(`/api/v2/shares/${z.uuid().parse(shareId)}/link`));
759
+ return shareCreatedSchema.parse(await this.api(`/api/v2/shares/${z.uuid().parse(shareId)}/link`));
629
760
  }
630
761
  async revoke(shareId) {
631
762
  return this.api(`/api/v2/shares/${z.uuid().parse(shareId)}`, "DELETE");
632
763
  }
633
- /** The bundle and who shared it, read with the link's capability. */
634
- async download(capability) {
635
- const route = `/api/v2/shared/${capabilitySchema.parse(capability)}`;
636
- const received = receivedSchema.parse(await this.api(route, "GET", void 0, true));
637
- const bytes = await this.request(`${route}/bundle`, "GET", void 0, true, MAX_SHARE_BYTES);
764
+ async revisions(shareId) {
765
+ return shareRevisionsSchema.parse(await this.api(`/api/v2/shares/${shareIdSchema.parse(shareId)}/revisions`));
766
+ }
767
+ /** Who shared a link's revision, and which revision is its latest. */
768
+ async shared(capability, revision) {
769
+ const query = revision === void 0 ? "" : `?rev=${shareIdSchema.parse(revision)}`;
770
+ return sharedMetaSchema.parse(await this.api(`/api/v2/shared/${capabilitySchema.parse(capability)}${query}`, "GET", void 0, true));
771
+ }
772
+ /** The bundle of the revision the meta names, and who shared it. */
773
+ async download(capability, revision) {
774
+ const meta = await this.shared(capability, revision);
775
+ const bytes = await this.request(`/api/v2/shared/${capability}/bundle?rev=${meta.revision.id}`, "GET", void 0, true, MAX_SHARE_BYTES);
638
776
  return {
639
- bundle: shareBundleSchema.parse(parseJsonText(new TextDecoder().decode(bytes))),
777
+ bundle: parseWhiteboardFile(new TextDecoder().decode(bytes)),
640
778
  attribution: {
641
- login: received.sender.login,
642
- sharedAt: received.sharedAt
643
- }
779
+ login: meta.sender.login,
780
+ sharedAt: meta.sharedAt
781
+ },
782
+ revision: meta.revision,
783
+ latest: meta.latest
644
784
  };
645
785
  }
646
786
  };
@@ -847,7 +987,7 @@ async function readShareRepository(root) {
847
987
  }
848
988
  //#endregion
849
989
  //#region src/sharing/import.ts
850
- const sharedSnapshotSchema = z.strictObject({
990
+ const sharedSnapshotSchema = z.object({
851
991
  reviewId: z.string().min(1),
852
992
  version: z.number().int().nonnegative(),
853
993
  /** Empty for a board never named; it reads "Untitled". */
@@ -859,7 +999,7 @@ const sharedSnapshotSchema = z.strictObject({
859
999
  document: documentSchema,
860
1000
  lenses: z.array(lensSchema).optional(),
861
1001
  createdAt: z.string(),
862
- origin: z.strictObject({
1002
+ origin: z.object({
863
1003
  branch: z.string().optional(),
864
1004
  baseRef: z.string().optional(),
865
1005
  baseBranch: z.string().optional(),
@@ -871,7 +1011,7 @@ const sharedSnapshotSchema = z.strictObject({
871
1011
  }).optional()
872
1012
  });
873
1013
  const count = z.number().int().nonnegative();
874
- const counts = z.strictObject({
1014
+ const counts = z.object({
875
1015
  additions: count,
876
1016
  deletions: count
877
1017
  });
@@ -881,9 +1021,9 @@ const mapSchema = softwareModelDataSchema.extend({
881
1021
  countsByElementPath: z.record(z.string(), counts),
882
1022
  unmappedByElementPath: z.record(z.string(), counts.extend({ files: z.array(counts.extend({
883
1023
  file: z.string(),
884
- hunks: z.array(z.strictObject({
1024
+ hunks: z.array(z.object({
885
1025
  startLine: count,
886
- lines: z.array(z.strictObject({
1026
+ lines: z.array(z.object({
887
1027
  kind: z.enum(["add", "remove"]),
888
1028
  oldLine: count.nullable(),
889
1029
  newLine: count.nullable(),
@@ -892,13 +1032,14 @@ const mapSchema = softwareModelDataSchema.extend({
892
1032
  }))
893
1033
  })) }))
894
1034
  });
895
- z.strictObject({ maps: z.record(z.string(), mapSchema) });
1035
+ z.object({ maps: z.record(z.string(), mapSchema) });
896
1036
  /** Commits and traces live in a checkout; a document alone needs none. */
897
1037
  function needsRepository(bundle, snapshot) {
898
1038
  return Boolean(snapshot.pins || referenceCommits(snapshot).length || Object.keys(bundle.traces).length);
899
1039
  }
900
1040
  function validateShareBundle(input) {
901
1041
  const bundle = shareBundleSchema.parse(input);
1042
+ if (bundle.frozen) throw new ReviewInputError("This whiteboard is a read-only snapshot of uncommitted changes. View it in the browser.", 409);
902
1043
  const stored = bundle.snapshot;
903
1044
  const snapshot = sharedSnapshotSchema.parse(stored.document === void 0 ? stored : liftSharedLenses(stored, migrateStoredDocument(stored.document)));
904
1045
  if (snapshot.reviewId !== bundle.reviewId || snapshot.version !== bundle.version || snapshot.title !== bundle.title) throw new Error("Shared snapshot does not match its bundle.");
@@ -948,12 +1089,41 @@ function shareKey(origin, capability) {
948
1089
  capabilitySchema.parse(capability);
949
1090
  return createHash("sha256").update(JSON.stringify([url.origin, capability])).digest("hex");
950
1091
  }
951
- /** Imports a share as an ordinary board, once per link: the link's copy
952
- * comes back while it exists, even from Trash. */
953
- async function importShare(input, key, received, attribution) {
1092
+ /** Which copy a served revision opens or refreshes in place; otherwise a new copy. */
1093
+ function planShareCopy(copies, served, pinned = false) {
1094
+ const same = copies.find(({ shared }) => shared.revisionId === void 0 ? shared.sharedAt === served.revision.createdAt : shared.revisionId === served.revision.id);
1095
+ if (same) return {
1096
+ kind: "open",
1097
+ reviewId: same.reviewId
1098
+ };
1099
+ const unedited = !pinned && served.revision.id === served.latest && copies.find((copy) => !copy.shared.pinned && copy.version === (copy.shared.importedVersion ?? 0));
1100
+ return unedited ? {
1101
+ kind: "refresh",
1102
+ reviewId: unedited.reviewId,
1103
+ version: unedited.version
1104
+ } : { kind: "import" };
1105
+ }
1106
+ /** Opens, refreshes or copies a link's board by the revision it serves. */
1107
+ async function receiveShare(input, key, served, download, pinned = false) {
1108
+ const plan = planShareCopy(input.store.copiesOf(key), served, pinned);
1109
+ if (plan.kind === "open") return plan.reviewId;
1110
+ const { bundle, attribution } = await download();
1111
+ const provenance = {
1112
+ ...attribution,
1113
+ revisionId: served.revision.id,
1114
+ ...pinned && { pinned }
1115
+ };
1116
+ if (plan.kind === "import") return importShare(input, key, bundle, provenance);
1117
+ try {
1118
+ return await importShare(input, key, bundle, provenance, plan);
1119
+ } catch (error) {
1120
+ if (error instanceof ReviewInputError && (error.status === 409 || error.status === 404)) return importShare(input, key, bundle, provenance);
1121
+ throw error;
1122
+ }
1123
+ }
1124
+ /** Imports a share as an ordinary board, or into a copy of its link. */
1125
+ async function importShare(input, key, received, provenance, into) {
954
1126
  const { store, data } = input;
955
- const existing = store.copyOf(key);
956
- if (existing) return existing;
957
1127
  const { bundle, snapshot, maps } = validateShareBundle(received);
958
1128
  const cloneUrl = bundle.repository?.cloneUrl;
959
1129
  let repositoryId;
@@ -988,7 +1158,7 @@ async function importShare(input, key, received, attribution) {
988
1158
  }
989
1159
  let document = JSON.stringify(snapshot.document);
990
1160
  for (const [id, copy] of ids) document = document.replaceAll(id, copy);
991
- const { reviewId } = await store.execute({ operation: {
1161
+ const create = { operation: {
992
1162
  type: "create",
993
1163
  ...snapshot.title && { title: snapshot.title },
994
1164
  ...pins ? { target: {
@@ -998,17 +1168,19 @@ async function importShare(input, key, received, attribution) {
998
1168
  kind: "project",
999
1169
  repositoryId
1000
1170
  } }
1001
- } }, {
1171
+ } };
1172
+ const seed = {
1002
1173
  document: JSON.parse(document),
1003
1174
  lenses: snapshot.lenses,
1004
1175
  origin: snapshot.origin,
1005
1176
  shared: {
1006
- ...attribution,
1177
+ ...provenance,
1007
1178
  cloneUrl,
1008
1179
  key
1009
1180
  },
1010
1181
  copiedFrom: snapshot.reviewId
1011
- }, { kind: "person" });
1182
+ };
1183
+ const { reviewId } = into ? await store.refreshCopy(into.reviewId, into.version, create, seed) : await store.execute(create, seed, { kind: "person" });
1012
1184
  return reviewId;
1013
1185
  }
1014
1186
  /** A checkout of the share's repository with every commit it reads at: the
@@ -1050,4 +1222,4 @@ async function shareRepository({ store, data, ...git }, cloneUrl, pins, commits,
1050
1222
  return repositoryId;
1051
1223
  }
1052
1224
  //#endregion
1053
- export { readBoundedStream as _, forgetSharedCommits as a, shareBundleSchema as b, ShareClient as c, legacyModeSchema as d, lensReport as f, StreamLimitError as g, uncategorizedReport as h, validateShareBundle as i, exportShare as l, reviewProgress as m, needsRepository as n, readShareRepository as o, progressUpdateSchema as p, shareKey as r, ShareAuthError as s, importShare as t, validateShareSources as u, SHARE_FORMAT as v, shareIdSchema as x, parseShareLink as y };
1225
+ export { StreamLimitError as C, shareIdSchema as D, shareBundleSchema as E, uncategorizedReport as S, parseShareLink as T, validateShareSources as _, shareKey as a, progressUpdateSchema as b, readShareRepository as c, ShareRequestError as d, parseWhiteboardFile as f, exportShare as g, whiteboardFileText as h, receiveShare as i, ShareAuthError as l, whiteboardFileName as m, needsRepository as n, validateShareBundle as o, whiteboardFileKey as p, planShareCopy as r, forgetSharedCommits as s, importShare as t, ShareClient as u, legacyModeSchema as v, readBoundedStream as w, reviewProgress as x, lensReport as y };
@@ -1,12 +1,12 @@
1
1
  import { S as splitGitPatchFiles, T as runVcsSync, _ as listTrackedFilesAtCommit, a as diff, b as resolveRepoContext, c as diffFileSummariesWorkingTree, d as git, f as gitAt, g as listCommitRange, h as jjRevisionIsConflicted, i as detectLocalVcs, l as diffTrees, m as gitCommonDirSync, n as createBlobBatchReader, p as gitCommonDir, s as diffFileSummariesTrees, t as LocalVcsToolsMissingError, u as diffWorkingTree, v as parseGitRemote, w as runVcs, x as resolveRevision, y as readFileAtCommit } from "./src-Y30gweiK.js";
2
2
  import { c as jsonString, m as isStringValue, n as isJsonObject, o as jsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
3
- import { A as parseMarkdown, B as lensSchema, D as summarizeEdit, E as sourceReferences, G as SourceRangeError, J as requireVisibleSource, K as checkSourcePath, M as checkReferences, O as markdownNodes, P as documentAdvice, R as applyLensEdit, S as respellEditLinks, V as lensSelections, Y as sliceSourceRange, a as boardLinks, at as unionIntervals, b as respellCommitLinks, c as documentSchema, ct as activityEndSchema, d as explicitPins, h as pinsSchema, i as assignMissingIds, it as sourceAnchors, k as markdownText, l as editSchema, lt as activityUpdateSchema, m as pinnedRepositoryIds, n as anchorPins, o as boardRepositoryId, ot as ReviewInputError, p as pinDocumentReferences, q as fileLineRangeSchema, r as applyEdit, s as commitLinkCommits, st as activityBeginSchema, t as adoptFlowUnits, tt as parseAnchor, u as elements, ut as focusSchema, v as resourceReference, w as reviewTargetSchema, x as respellDocumentLinks, y as resourceReferences, z as lensEditSchema } from "./src-B2b8cCoI.js";
4
- import { F as applyStructuralUpdate, I as decodeStructuralDiffEvent, L as structuralChangeCounts, c as boardIconKind, l as boardTitle } from "./src-DLWol7GZ.js";
5
- import { s as safeStorageSegment } from "./review-home-paths-LdGS4SpR.js";
6
- import { A as updateCoverage, C as coverageSchema, E as intersectIntervals, S as parseUnifiedPatch, T as emptyCoverage, _ as fetchPullRequest, a as SoftwareModelValidationError, b as textIncludesQuote, c as traceSchema, d as setPullRequest, f as sourceBranchName, g as defaultPullRequestDeps, h as GitHubRefusedRead, i as readStoredDocument, k as subtractIntervals, l as pullRequestKey, m as resolvePullRequest, n as migrateStoredDocument, o as defineSoftwareMap, r as migrateStoredLenses, u as pullRequestUrl, v as remoteUrls, x as textualRows, y as azureRepositoryOf } from "./stored-document-migration-iwIQ4l5G.js";
3
+ import { A as parseMarkdown, B as lensSchema, D as summarizeEdit, E as sourceReferences, G as SourceRangeError, J as requireVisibleSource, K as checkSourcePath, M as checkReferences, O as markdownNodes, P as documentAdvice, R as applyLensEdit, S as respellEditLinks, V as lensSelections, Y as sliceSourceRange, a as boardLinks, at as unionIntervals, b as respellCommitLinks, c as documentSchema, ct as activityEndSchema, d as explicitPins, h as pinsSchema, i as assignMissingIds, it as sourceAnchors, k as markdownText, l as editSchema, lt as activityUpdateSchema, m as pinnedRepositoryIds, n as anchorPins, o as boardRepositoryId, ot as ReviewInputError, p as pinDocumentReferences, q as fileLineRangeSchema, r as applyEdit, s as commitLinkCommits, st as activityBeginSchema, t as adoptFlowUnits, tt as parseAnchor, u as elements, ut as focusSchema, v as resourceReference, w as reviewTargetSchema, x as respellDocumentLinks, y as resourceReferences, z as lensEditSchema } from "./src-BQDlJXkG.js";
4
+ import { I as applyStructuralUpdate, L as decodeStructuralDiffEvent, R as structuralChangeCounts, c as boardIconKind, l as boardTitle } from "./src-CrrW7lcY.js";
5
+ import { s as safeStorageSegment } from "./review-home-paths-BmZX2qiz.js";
6
+ import { A as updateCoverage, C as coverageSchema, E as intersectIntervals, S as parseUnifiedPatch, T as emptyCoverage, _ as fetchPullRequest, a as SoftwareModelValidationError, b as textIncludesQuote, c as traceSchema, d as setPullRequest, f as sourceBranchName, g as defaultPullRequestDeps, h as GitHubRefusedRead, i as readStoredDocument, k as subtractIntervals, l as pullRequestKey, m as resolvePullRequest, n as migrateStoredDocument, o as defineSoftwareMap, r as migrateStoredLenses, u as pullRequestUrl, v as remoteUrls, x as textualRows, y as azureRepositoryOf } from "./stored-document-migration-D1wK4yID.js";
7
7
  import { A as processStartIdentity, L as writePrivateJsonAtomic, O as pidReused, j as withFileLock, k as processIsAlive } from "./s3-config-CjcLyFV5.js";
8
8
  import { t as errorMessage } from "./error-message-OtiDonty.js";
9
- import { a as askHistoryEntrySchema, i as askEntrySchema, m as isMissingFileError, o as askOfferSchema, s as askPermissionsSchema } from "./thread-state-B7L1mpN_.js";
9
+ import { a as askHistoryEntrySchema, i as askEntrySchema, m as isMissingFileError, o as askOfferSchema, s as askPermissionsSchema } from "./thread-state-WTqSborE.js";
10
10
  import { createRequire } from "node:module";
11
11
  import crypto, { createHash, randomUUID } from "node:crypto";
12
12
  import { z } from "zod";
@@ -3095,11 +3095,16 @@ var ReviewStore = class {
3095
3095
  this.pending = run.catch(() => {});
3096
3096
  return run;
3097
3097
  }
3098
- /** The board copied from a share link, until it is deleted. */
3099
- copyOf(key) {
3100
- const row = this.db.prepare(`SELECT reviews.id FROM reviews JOIN versions ON versions.review_id=reviews.id AND versions.version=reviews.version
3101
- WHERE CASE WHEN json_valid(versions.snapshot) THEN json_extract(versions.snapshot,'$.shared.key') END=?`).get(key);
3102
- return row && String(row.id);
3098
+ /** The boards copied from a share link, newest first, until each is deleted. */
3099
+ copiesOf(key) {
3100
+ return this.db.prepare(`SELECT reviews.id, reviews.version, json_extract(versions.snapshot,'$.shared') AS shared
3101
+ FROM reviews JOIN versions ON versions.review_id=reviews.id AND versions.version=reviews.version
3102
+ WHERE CASE WHEN json_valid(versions.snapshot) THEN json_extract(versions.snapshot,'$.shared.key') END=?
3103
+ ORDER BY reviews.rowid DESC`).all(key).map((row) => ({
3104
+ reviewId: String(row.id),
3105
+ version: Number(row.version),
3106
+ shared: JSON.parse(String(row.shared))
3107
+ }));
3103
3108
  }
3104
3109
  /** Checkouts whose remote is cloneUrl, most recently used first. */
3105
3110
  checkoutsOf(cloneUrl) {
@@ -3361,6 +3366,21 @@ var ReviewStore = class {
3361
3366
  pullRequest?.catch(() => {});
3362
3367
  const pullRequestTitle = pullRequest ? pullRequest.then(({ title }) => title || void 0, () => void 0) : this.startPullRequestTitle(command);
3363
3368
  const prepared = this.prepareCommand(command, pullRequest, pullRequestTitle, author).then((source) => this.applyPreparedCommand(source, initial, author));
3369
+ return this.track(prepared);
3370
+ }
3371
+ /** Replaces a share copy's content under the same board id; 409 once it moved past `baseVersion`. */
3372
+ refreshCopy(reviewId, baseVersion, create, seed) {
3373
+ if (this.closing) return Promise.reject(/* @__PURE__ */ new Error("Review store is closing."));
3374
+ const command = commandSchema.parse(create);
3375
+ const op = command.operation;
3376
+ if (op.type !== "create" || op.pullRequestUrl || op.parentId || op.dependsOn || op.projectId) throw new ReviewInputError("A refresh takes a create command with a title and target alone.");
3377
+ const person = { kind: "person" };
3378
+ return this.track(this.prepareCommand(command, void 0, Promise.resolve(void 0), person).then((source) => this.applyPreparedCommand(source, seed, person, {
3379
+ reviewId,
3380
+ baseVersion
3381
+ })));
3382
+ }
3383
+ track(prepared) {
3364
3384
  this.preparing.add(prepared);
3365
3385
  prepared.finally(() => this.preparing.delete(prepared)).catch(() => {});
3366
3386
  return prepared;
@@ -3389,10 +3409,10 @@ var ReviewStore = class {
3389
3409
  resolvedTitle: await pullRequestTitle
3390
3410
  };
3391
3411
  }
3392
- applyPreparedCommand(prepared, initial, author) {
3412
+ applyPreparedCommand(prepared, initial, author, into) {
3393
3413
  const { op, requestedTarget, fromPullRequest, resolvedTarget, resolvedBranch, discoveredPullRequest, resolvedTitle } = prepared;
3394
3414
  return this.enqueue(async () => {
3395
- if (op.type === "create" && (op.pullRequestUrl ? op.reuseExisting !== false : op.reuseExisting && resolvedTarget?.pins)) {
3415
+ if (!into && op.type === "create" && (op.pullRequestUrl ? op.reuseExisting !== false : op.reuseExisting && resolvedTarget?.pins)) {
3396
3416
  const known = op.pullRequestUrl ? this.reviewsForPullRequest(op.pullRequestUrl) : [];
3397
3417
  const branch = fromPullRequest?.branch ?? resolvedBranch;
3398
3418
  const repository = resolvedTarget?.target.repositoryId;
@@ -3410,8 +3430,9 @@ var ReviewStore = class {
3410
3430
  if (op.type === "lifecycle") return this.commitBoardFacts(op.reviewId, author, { state: op.state });
3411
3431
  if (op.type === "attention") return this.commitBoardFacts(op.reviewId, author, { viewed: true });
3412
3432
  if (op.type === "delete") return this.deleteBoard(this.read(op.reviewId), author, { fromTrash: true });
3413
- const id = op.type === "create" ? randomUUID() : op.reviewId;
3414
- const previous = op.type === "create" ? void 0 : this.read(id);
3433
+ const id = into?.reviewId ?? (op.type === "create" ? randomUUID() : op.reviewId);
3434
+ const previous = op.type === "create" && !into ? void 0 : this.read(id);
3435
+ if (into && (!previous?.shared?.key || previous.shared.key !== initial?.shared?.key)) throw new ReviewInputError("Only a copy of the same share link refreshes in place.");
3415
3436
  let snapshot = op.type === "create" ? createdSnapshot(id, op, resolvedTarget, fromPullRequest && (fromPullRequest.title || `PR #${op.pullRequestUrl.split("/").at(-1)}`), author.kind === "person") : structuredClone(previous);
3416
3437
  delete snapshot.trashedAt;
3417
3438
  delete snapshot.sourceUnavailable;
@@ -3520,9 +3541,11 @@ var ReviewStore = class {
3520
3541
  restore: op.type === "restore",
3521
3542
  repin: op.type === "set_target",
3522
3543
  copied: initial !== void 0,
3544
+ inPlace: into !== void 0,
3545
+ baseVersion: into?.baseVersion,
3523
3546
  projectLive: op.type !== "restore" && !resolvedTarget,
3524
3547
  result: {
3525
- ...op.type === "create" && { created: true },
3548
+ ...op.type === "create" && !into && { created: true },
3526
3549
  targetId: applied?.targetId ?? lensTarget?.targetId,
3527
3550
  ...applied && { type: applied.type },
3528
3551
  ...lensTarget && { type: lensTarget.type },
@@ -3590,6 +3613,7 @@ var ReviewStore = class {
3590
3613
  person: author.kind === "person"
3591
3614
  }));
3592
3615
  warnings = [...change.warnings ?? [], ...warnings];
3616
+ if (change.copied && snapshot.shared) snapshot.shared.importedVersion = snapshot.version;
3593
3617
  const result = {
3594
3618
  ...change.result,
3595
3619
  reviewId: id,
@@ -3611,7 +3635,8 @@ var ReviewStore = class {
3611
3635
  if (change.relations?.dependsOn) this.setDependencies(id, change.relations.dependsOn);
3612
3636
  if (previous && projectId !== previousProject && this.dropRelationsOutside(id, projectId)) result.warnings = [...result.warnings ?? [], "Its parent, children and dependencies were in its old project, so they were dropped: whiteboards relate only within a project."];
3613
3637
  }, previous ? () => {
3614
- this.assertWritable(id);
3638
+ if (change.inPlace) this.assertExists(id);
3639
+ else this.assertWritable(id);
3615
3640
  if (change.baseVersion !== void 0) assertBaseVersion(change.baseVersion, this.currentVersion(id));
3616
3641
  else this.assertMutation(id, previous.version);
3617
3642
  holds();
@@ -1,8 +1,8 @@
1
1
  import { n as isJsonObject } from "./src-X9phtB2j.js";
2
- import { ot as ReviewInputError } from "./src-B2b8cCoI.js";
3
- import { a as toolResultText, s as ReviewApiError, t as ToolText } from "./agent-client-CMacGbMp.js";
4
- import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-CiLy46kh.js";
5
- import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-RLR8m9hq.js";
2
+ import { ot as ReviewInputError } from "./src-BQDlJXkG.js";
3
+ import { a as toolResultText, s as ReviewApiError, t as ToolText } from "./agent-client-BCEbSPWU.js";
4
+ import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-CkfxTVJm.js";
5
+ import { a as renderedImage, i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-YOFGHJCZ.js";
6
6
  import { Server } from "@modelcontextprotocol/server";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
8
8
  //#region src/review-api/mcp-client-agent.ts
@@ -118,10 +118,13 @@ async function serveReviewMcp(connect, stdin, stdout, stderr = process.stderr, t
118
118
  phase = "call";
119
119
  tool = tools.find((tool) => tool.name === request.params.name);
120
120
  if (!tool) throw new ReviewInputError(`Unknown Whiteboard tool: ${request.params.name}`);
121
- const result = await callPublicTool(client, tool, request.params.arguments ?? {}, ctx.mcpReq.signal);
121
+ const { image, result } = renderedImage(tool, await callPublicTool(client, tool, request.params.arguments ?? {}, ctx.mcpReq.signal));
122
122
  const text = toolResultText(tool, result);
123
123
  report();
124
- const response = { content: [{
124
+ const response = { content: [...image ? [{
125
+ type: "image",
126
+ ...image
127
+ }] : [], {
125
128
  type: "text",
126
129
  text: listedWhileDown && tool.name === instructionsTool.name ? `${RELOAD_TOOLS}\n\n${text}` : text
127
130
  }] };