@tempo-ai/mcp 0.0.105 → 0.0.106

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.
@@ -1342,8 +1342,8 @@ var require_utils = __commonJS({
1342
1342
  }
1343
1343
  return output;
1344
1344
  };
1345
- exports.basename = (path61, { windows } = {}) => {
1346
- const segs = path61.split(windows ? /[\\/]/ : "/");
1345
+ exports.basename = (path62, { windows } = {}) => {
1346
+ const segs = path62.split(windows ? /[\\/]/ : "/");
1347
1347
  const last2 = segs[segs.length - 1];
1348
1348
  if (last2 === "") {
1349
1349
  return segs[segs.length - 2];
@@ -3645,10 +3645,10 @@ var require_src2 = __commonJS({
3645
3645
  var fs_1 = __require("fs");
3646
3646
  var debug_1 = __importDefault(require_src());
3647
3647
  var log = debug_1.default("@kwsites/file-exists");
3648
- function check(path61, isFile, isDirectory) {
3649
- log(`checking %s`, path61);
3648
+ function check(path62, isFile, isDirectory) {
3649
+ log(`checking %s`, path62);
3650
3650
  try {
3651
- const stat11 = fs_1.statSync(path61);
3651
+ const stat11 = fs_1.statSync(path62);
3652
3652
  if (stat11.isFile() && isFile) {
3653
3653
  log(`[OK] path represents a file`);
3654
3654
  return true;
@@ -3668,8 +3668,8 @@ var require_src2 = __commonJS({
3668
3668
  throw e;
3669
3669
  }
3670
3670
  }
3671
- function exists3(path61, type = exports.READABLE) {
3672
- return check(path61, (type & exports.FILE) > 0, (type & exports.FOLDER) > 0);
3671
+ function exists3(path62, type = exports.READABLE) {
3672
+ return check(path62, (type & exports.FILE) > 0, (type & exports.FOLDER) > 0);
3673
3673
  }
3674
3674
  exports.exists = exists3;
3675
3675
  exports.FILE = 1;
@@ -5692,7 +5692,7 @@ var require_png = __commonJS({
5692
5692
  var Parser = require_parser_async();
5693
5693
  var Packer = require_packer_async();
5694
5694
  var PNGSync = require_png_sync();
5695
- var PNG4 = exports.PNG = function(options) {
5695
+ var PNG5 = exports.PNG = function(options) {
5696
5696
  Stream.call(this);
5697
5697
  options = options || {};
5698
5698
  this.width = options.width | 0;
@@ -5721,9 +5721,9 @@ var require_png = __commonJS({
5721
5721
  this._parser.on("close", this._handleClose.bind(this));
5722
5722
  this._packer.on("error", this.emit.bind(this, "error"));
5723
5723
  };
5724
- util.inherits(PNG4, Stream);
5725
- PNG4.sync = PNGSync;
5726
- PNG4.prototype.pack = function() {
5724
+ util.inherits(PNG5, Stream);
5725
+ PNG5.sync = PNGSync;
5726
+ PNG5.prototype.pack = function() {
5727
5727
  if (!this.data || !this.data.length) {
5728
5728
  this.emit("error", "No data provided");
5729
5729
  return this;
@@ -5735,7 +5735,7 @@ var require_png = __commonJS({
5735
5735
  );
5736
5736
  return this;
5737
5737
  };
5738
- PNG4.prototype.parse = function(data, callback) {
5738
+ PNG5.prototype.parse = function(data, callback) {
5739
5739
  if (callback) {
5740
5740
  let onParsed, onError2;
5741
5741
  onParsed = function(parsedData) {
@@ -5753,27 +5753,27 @@ var require_png = __commonJS({
5753
5753
  this.end(data);
5754
5754
  return this;
5755
5755
  };
5756
- PNG4.prototype.write = function(data) {
5756
+ PNG5.prototype.write = function(data) {
5757
5757
  this._parser.write(data);
5758
5758
  return true;
5759
5759
  };
5760
- PNG4.prototype.end = function(data) {
5760
+ PNG5.prototype.end = function(data) {
5761
5761
  this._parser.end(data);
5762
5762
  };
5763
- PNG4.prototype._metadata = function(metadata) {
5763
+ PNG5.prototype._metadata = function(metadata) {
5764
5764
  this.width = metadata.width;
5765
5765
  this.height = metadata.height;
5766
5766
  this.emit("metadata", metadata);
5767
5767
  };
5768
- PNG4.prototype._gamma = function(gamma) {
5768
+ PNG5.prototype._gamma = function(gamma) {
5769
5769
  this.gamma = gamma;
5770
5770
  };
5771
- PNG4.prototype._handleClose = function() {
5771
+ PNG5.prototype._handleClose = function() {
5772
5772
  if (!this._parser.writable && !this._packer.readable) {
5773
5773
  this.emit("close");
5774
5774
  }
5775
5775
  };
5776
- PNG4.bitblt = function(src, dst, srcX, srcY, width, height, deltaX, deltaY) {
5776
+ PNG5.bitblt = function(src, dst, srcX, srcY, width, height, deltaX, deltaY) {
5777
5777
  srcX |= 0;
5778
5778
  srcY |= 0;
5779
5779
  width |= 0;
@@ -5795,11 +5795,11 @@ var require_png = __commonJS({
5795
5795
  );
5796
5796
  }
5797
5797
  };
5798
- PNG4.prototype.bitblt = function(dst, srcX, srcY, width, height, deltaX, deltaY) {
5799
- PNG4.bitblt(this, dst, srcX, srcY, width, height, deltaX, deltaY);
5798
+ PNG5.prototype.bitblt = function(dst, srcX, srcY, width, height, deltaX, deltaY) {
5799
+ PNG5.bitblt(this, dst, srcX, srcY, width, height, deltaX, deltaY);
5800
5800
  return this;
5801
5801
  };
5802
- PNG4.adjustGamma = function(src) {
5802
+ PNG5.adjustGamma = function(src) {
5803
5803
  if (src.gamma) {
5804
5804
  for (let y = 0; y < src.height; y++) {
5805
5805
  for (let x = 0; x < src.width; x++) {
@@ -5814,8 +5814,8 @@ var require_png = __commonJS({
5814
5814
  src.gamma = 0;
5815
5815
  }
5816
5816
  };
5817
- PNG4.prototype.adjustGamma = function() {
5818
- PNG4.adjustGamma(this);
5817
+ PNG5.prototype.adjustGamma = function() {
5818
+ PNG5.adjustGamma(this);
5819
5819
  };
5820
5820
  }
5821
5821
  });
@@ -5986,7 +5986,7 @@ var TOOL_MANIFEST = [
5986
5986
  // ── linear (4) ──────────────────────────────────────────────────────────
5987
5987
  ...read("linear", ["linear_search_issues", "linear_get_issue"]),
5988
5988
  ...write("linear", ["linear_update_issue", "linear_add_comment"]),
5989
- // ── canvas (29) ─────────────────────────────────────────────────────────
5989
+ // ── canvas (30) ─────────────────────────────────────────────────────────
5990
5990
  // File/workspace tools — work anywhere the repo checkout lives.
5991
5991
  ...read("canvas", [
5992
5992
  "canvas_validate",
@@ -6011,7 +6011,7 @@ var TOOL_MANIFEST = [
6011
6011
  // Visual tools — powered externally by the headless capture hooks
6012
6012
  // (disposable devserver + Playwright); error actionably when Chromium
6013
6013
  // is not installed.
6014
- ...read("canvas", ["canvas_screenshot"]),
6014
+ ...read("canvas", ["canvas_screenshot", "canvas_verify_vrt"]),
6015
6015
  ...write("canvas", ["canvas_share"]),
6016
6016
  // App-internal surface: devserver supervision is the CLI's own plumbing
6017
6017
  // (spawned per capture), and the editing-lock bookends coordinate the
@@ -6443,8 +6443,8 @@ var STAGE_RANK_END = Number.MAX_SAFE_INTEGER;
6443
6443
  // ../issues-core/share-url.ts
6444
6444
  function buildIssueShareUrl(baseUrl, orgSlugOrId, humanId, titleSlug) {
6445
6445
  const base = `/${encodeURIComponent(orgSlugOrId)}/issues/${encodeURIComponent(humanId)}`;
6446
- const path61 = titleSlug ? `${base}/${encodeURIComponent(titleSlug)}` : base;
6447
- const u = new URL(path61, baseUrl);
6446
+ const path62 = titleSlug ? `${base}/${encodeURIComponent(titleSlug)}` : base;
6447
+ const u = new URL(path62, baseUrl);
6448
6448
  return u.toString();
6449
6449
  }
6450
6450
 
@@ -7916,19 +7916,19 @@ function normalizeRepoUrl(url) {
7916
7916
  const sshMatch = cleaned.match(sshShorthand);
7917
7917
  if (sshMatch) {
7918
7918
  const host = sshMatch[1];
7919
- const path61 = sshMatch[2];
7920
- return finalize(host, path61);
7919
+ const path62 = sshMatch[2];
7920
+ return finalize(host, path62);
7921
7921
  }
7922
7922
  const protocolMatch = cleaned.match(/^[\w+.-]+:\/\/(?:[^@]+@)?([\w.-]+)\/([\w./-]+)$/);
7923
7923
  if (protocolMatch) {
7924
7924
  const host = protocolMatch[1];
7925
- const path61 = protocolMatch[2];
7926
- return finalize(host, path61);
7925
+ const path62 = protocolMatch[2];
7926
+ return finalize(host, path62);
7927
7927
  }
7928
7928
  return null;
7929
7929
  }
7930
- function finalize(host, path61) {
7931
- let cleaned = path61.replace(/\.git$/, "");
7930
+ function finalize(host, path62) {
7931
+ let cleaned = path62.replace(/\.git$/, "");
7932
7932
  cleaned = cleaned.replace(/^\/+|\/+$/g, "");
7933
7933
  if (!cleaned) return null;
7934
7934
  return `${host}/${cleaned}`.toLowerCase();
@@ -9528,8 +9528,8 @@ var api2 = anyApi2;
9528
9528
  // ../doc-editor/src/client/docShareUrl.ts
9529
9529
  function buildDocShareUrl(baseUrl, orgSlugOrId, docId, titleSlug) {
9530
9530
  const base = `/${encodeURIComponent(orgSlugOrId)}/docs/${encodeURIComponent(docId)}`;
9531
- const path61 = titleSlug ? `${base}/${encodeURIComponent(titleSlug)}` : base;
9532
- return new URL(path61, baseUrl).toString();
9531
+ const path62 = titleSlug ? `${base}/${encodeURIComponent(titleSlug)}` : base;
9532
+ return new URL(path62, baseUrl).toString();
9533
9533
  }
9534
9534
 
9535
9535
  // ../doc-editor/mcp/tools/read.ts
@@ -16954,6 +16954,7 @@ Screenshot ONE representative storyboard you actually edited \u2014 not every st
16954
16954
  - A \`canvas_not_open\` error means the user does not have any canvas open in the Design tab. Stop calling \`canvas_screenshot\`, tell the user to open the canvas, and move on \u2014 firing more screenshots will not change the outcome.
16955
16955
  - Any typed error (\`storyboard_not_found\`, \`storyboard_empty\`, \`capture_timeout\`, \`capture_threw\`, \`canvas_mismatch\`, \`capture_unsupported\`) \u2014 one try per storyboard. Don't retry, don't broaden to other storyboards, don't fire a second wave. **One exception:** a route storyboard's \`capture_threw\` "could not be cold-captured" means the app dev server isn't running \u2014 call \`start_app_dev_server\` once, then a single retry is warranted (better, pre-flight per the route-storyboard rule above so it never fails).
16956
16956
  - If you need to understand the failure, call \`canvas_debug({ canvas_slug, storyboard? })\` once and inspect the diagnostics instead of repeatedly calling \`canvas_screenshot\`.
16957
+ - \`canvas_verify_vrt\` adds its own typed errors, also one try each: \`storyboard_not_tested\` (add a literal \`vrt\` prop first), \`storyboard_paused\` (remove \`skip: true\` before verifying), \`storyboard_unsupported\` (route/HTML storyboards are not verified \u2014 component storyboards only), \`storyboard_invalid\` (parse error, or \`vrt.ignore\` masks \u226590% of the board), \`capture_incomplete\` / \`compare_failed\` (the batch or the pixel comparison broke \u2014 \`canvas_debug\` once), \`determinism_setup_failed\` (the browser could not install frozen clocks/timezone \u2014 an environment problem, not the storyboard's). An \`unstable\` verdict is NOT an error: fix the nondeterministic input and rerun.
16957
16958
 
16958
16959
  Quality checklist:
16959
16960
 
@@ -16978,6 +16979,27 @@ For UI that already exists, measure instead of guessing:
16978
16979
 
16979
16980
  ---
16980
16981
 
16982
+ ## 9.2 VRT stability (\`canvas_verify_vrt\`)
16983
+
16984
+ A storyboard carrying the literal \`vrt\` prop is a **tested** storyboard: the visual-review workflow captures it on every push and diffs it against a baseline. A tested storyboard that renders differently on each mount (live clocks, \`Math.random\`, unseeded fixtures, in-flight animation, network-dependent content) flakes the \`visual-review\` status for every PR after yours. So after adding or changing \`vrt\`, run
16985
+
16986
+ \`\`\`
16987
+ canvas_verify_vrt({ canvas_slug, storyboard })
16988
+ \`\`\`
16989
+
16990
+ once per tested storyboard you touched, and keep it in the same session as the edit.
16991
+
16992
+ What it does: one capture session, \`runs\` (default 3, max 10) fresh isolated mounts at full resolution, each compared with run 1 under the exact visual-review policy \u2014 VRT's frozen clocks, seeded randomness and motion controls on the render side; the same AA-tolerant pixel matcher, the storyboard's \`threshold\` (default 0.1), and its \`ignore\` masks on the compare side. Nothing is retained between runs except run 1 and the running diff. It measures rendered output only; it never reads or edits source.
16993
+
16994
+ Reading the result \u2014 only \`vrt_stable\` matters:
16995
+
16996
+ - \`vrt_stable: true\` \u2014 done; the baseline will hold.
16997
+ - \`vrt_stable: false\` \u2014 the response carries \`diff_bounds_device_px\` (union of every change, in device pixels), per-run \`comparisons\`, and a fuchsia **diff-mask image** (transparent = stable, fuchsia = changed in at least one run; masks wider than 1990px are area-resampled so a one-pixel flicker still shows, while the JSON bounds stay in original device pixels). Use the region to walk the storyboard's component dependency graph for the nondeterministic input, replace it with fixed storyboard props / fixtures / a seeded source, and rerun. Raise \`runs\` for a flake that only shows one time in five.
16998
+
16999
+ It accepts only ACTIVE tested component storyboards: no \`vrt\` prop, \`vrt={{ skip: true }}\`, route storyboards, and HTML storyboards are all refused with a typed error (codes in the stop-conditions list above). Like \`canvas_screenshot\`, it fails fast with \`capacity_exceeded\` when every managed devserver slot is pinned \u2014 continue and tell the user to rerun rather than looping.
17000
+
17001
+ ---
17002
+
16981
17003
  ## 10. Constraints
16982
17004
 
16983
17005
  - No UUIDs, no JSON canvas configs
@@ -17905,9 +17927,9 @@ function buildCanvasInstructions(canvasesDir, options) {
17905
17927
  Before creating, modifying, restyling, or reviewing ANY UI, call \`asset_list\` FIRST and reuse results per \`usageInstructions\`. Do the same for design system, component library, or UI kit requests; use \`asset_list_libraries\` for a collection overview. To ADD or EDIT asset declarations, FIRST call get_guide({ topic: "tempo-assets-instructions" }) \u2014 \`defineAsset\` declares the TOP-LEVEL component consumers import (\`Select\`, never \`SelectTrigger\`).
17906
17928
 
17907
17929
  `;
17908
- return `Tempo's "Design" tab is a code-first Figma alternative. Canvases live under the canvases root ${canvasesDir}/, whose storyboards render PRODUCTION components and routes \u2014 never mocks of existing UI.
17930
+ return `Tempo canvases are code-first. Canvases live under the canvases root ${canvasesDir}/, whose storyboards render PRODUCTION components and routes \u2014 never mocks of existing UI.
17909
17931
 
17910
- Before any canvas change \u2014 including tempo.config.json edits (Docker, custom launchers, ports) \u2014 you MUST call get_guide({ topic: "tempo-canvas-instructions" }); it holds every protocol and overrides this summary.
17932
+ Before any canvas or tempo.config.json change, you MUST call get_guide({ topic: "tempo-canvas-instructions" }); it is authoritative.
17911
17933
 
17912
17934
  ${assetLibrarySection}## Hard invariants
17913
17935
 
@@ -17916,12 +17938,13 @@ ${assetLibrarySection}## Hard invariants
17916
17938
  - Bookend every canvas session: start_working_on_canvas BEFORE the first mutating call (canvas_create included) \u2192 edits \u2192 canvas_screenshot on ONE edited storyboard \u2192 finish_working_on_canvas once. On screenshot failure: canvas_debug once, no retries.
17917
17939
  - Write is blocked on *.canvas.tsx \u2014 canvas_create creates, Edit modifies.
17918
17940
  - Route storyboards need the user's real app: set_app_dev_command once, start_app_dev_server before screenshots.
17941
+ - After adding/changing \`vrt\`, run canvas_verify_vrt; fix nondeterminism and rerun until \`vrt_stable\`.
17919
17942
  - Replicating existing UI (live page, Browser tab, URL)? Pixel parity is the goal: capture its screenshot/HTML/styles via the CDP endpoint, loop canvas_screenshot against it \u2014 guide \xA79.1.
17920
17943
  - Link new canvases to sibling issue/doc via link_items (branch pushed).`;
17921
17944
  }
17922
17945
 
17923
17946
  // ../canvas-mcp/tools.ts
17924
- import path54 from "path";
17947
+ import path55 from "path";
17925
17948
 
17926
17949
  // ../asset-libraries/mcp/tools.ts
17927
17950
  import { readFile as readFile4, writeFile } from "fs/promises";
@@ -19782,7 +19805,7 @@ function parseCanvasFile(filePath, source) {
19782
19805
  "name",
19783
19806
  objectStringBindings
19784
19807
  );
19785
- const path61 = readStringAttribute(openingElement, "path");
19808
+ const path62 = readStringAttribute(openingElement, "path");
19786
19809
  const backgroundColor = readResolvableStringAttribute(
19787
19810
  openingElement,
19788
19811
  "backgroundColor",
@@ -19801,7 +19824,7 @@ function parseCanvasFile(filePath, source) {
19801
19824
  }
19802
19825
  return {
19803
19826
  filePath,
19804
- canvasPath: path61?.value,
19827
+ canvasPath: path62?.value,
19805
19828
  pageName: name?.value ?? fallbackPageName(filePath),
19806
19829
  backgroundColor: backgroundColor?.value,
19807
19830
  canvasElementSpan: toSpan(canvasElement),
@@ -19809,8 +19832,8 @@ function parseCanvasFile(filePath, source) {
19809
19832
  canvasChildrenInsertionOffset: typeof canvasElement?.closingElement?.start === "number" ? canvasElement.closingElement.start : void 0,
19810
19833
  nameSpan: name?.valueSpan,
19811
19834
  nameAttributeSpan: name?.attributeSpan,
19812
- pathSpan: path61?.valueSpan,
19813
- pathAttributeSpan: path61?.attributeSpan,
19835
+ pathSpan: path62?.valueSpan,
19836
+ pathAttributeSpan: path62?.attributeSpan,
19814
19837
  backgroundColorSpan: backgroundColor?.valueSpan,
19815
19838
  backgroundColorAttributeSpan: backgroundColor?.attributeSpan,
19816
19839
  imports: imports.all,
@@ -20678,7 +20701,7 @@ function adaptReactCanvasPage(filePath, source) {
20678
20701
  ...storyboard.route ? { route: storyboard.route } : {},
20679
20702
  ...storyboard.filepath ? { filepath: storyboard.filepath } : {},
20680
20703
  ...storyboard.src ? { src: storyboard.src } : {},
20681
- ...storyboard.vrt ? { vrt: true } : {},
20704
+ ...storyboard.vrt ? { vrt: true, vrtConfig: storyboard.vrt } : {},
20682
20705
  layoutSpans: toLegacyLayoutSpans(storyboard.layoutSpans),
20683
20706
  ...storyboard.errors.length > 0 ? {
20684
20707
  error: storyboard.errors.map((error) => error.message).join("; ")
@@ -22474,7 +22497,7 @@ function createQueuedFileEditor() {
22474
22497
  };
22475
22498
  return runAtIndex(0);
22476
22499
  }
22477
- async function readFile26(absolutePath) {
22500
+ async function readFile27(absolutePath) {
22478
22501
  return withFileLock(absolutePath, () => readFileUnlocked(absolutePath));
22479
22502
  }
22480
22503
  async function readBinaryFile(absolutePath) {
@@ -22560,7 +22583,7 @@ function createQueuedFileEditor() {
22560
22583
  });
22561
22584
  }
22562
22585
  return {
22563
- readFile: readFile26,
22586
+ readFile: readFile27,
22564
22587
  readBinaryFile,
22565
22588
  readFileIfExists,
22566
22589
  writeFile: writeFile11,
@@ -22572,7 +22595,7 @@ function createQueuedFileEditor() {
22572
22595
  unlinkFile,
22573
22596
  withFileLock,
22574
22597
  withFileLocks,
22575
- readWorkspaceFile: (workspacePath, relativePath) => readFile26(resolveWorkspaceRelativePath(workspacePath, relativePath)),
22598
+ readWorkspaceFile: (workspacePath, relativePath) => readFile27(resolveWorkspaceRelativePath(workspacePath, relativePath)),
22576
22599
  readWorkspaceFileIfExists: (workspacePath, relativePath) => readFileIfExists(resolveWorkspaceRelativePath(workspacePath, relativePath)),
22577
22600
  writeWorkspaceFile: (workspacePath, relativePath, content, expectedMtime, nextMtime) => writeFile11(
22578
22601
  resolveWorkspaceRelativePath(workspacePath, relativePath),
@@ -24580,8 +24603,8 @@ function extractImporterFromLine(line) {
24580
24603
  }
24581
24604
  return null;
24582
24605
  }
24583
- function normalizeImporterPath(path61) {
24584
- return path61.replace(/^\.\//, "");
24606
+ function normalizeImporterPath(path62) {
24607
+ return path62.replace(/^\.\//, "");
24585
24608
  }
24586
24609
 
24587
24610
  // ../tempo-devserver/port-listener.ts
@@ -26718,10 +26741,10 @@ async function scanTree(realRoot, ignorePaths = []) {
26718
26741
  for (let i = 0; i < pendingStats.length; i += STAT_CONCURRENCY) {
26719
26742
  const slice = pendingStats.slice(i, i + STAT_CONCURRENCY);
26720
26743
  await Promise.all(
26721
- slice.map(async (path61) => {
26744
+ slice.map(async (path62) => {
26722
26745
  try {
26723
- const st = await lstat(path61);
26724
- scan.set(path61, {
26746
+ const st = await lstat(path62);
26747
+ scan.set(path62, {
26725
26748
  mtimeMs: st.mtimeMs,
26726
26749
  size: st.size,
26727
26750
  isDir: false
@@ -26735,16 +26758,16 @@ async function scanTree(realRoot, ignorePaths = []) {
26735
26758
  }
26736
26759
  function diffScans(previous, next) {
26737
26760
  const events = [];
26738
- for (const [path61, now] of next) {
26739
- const before = previous.get(path61);
26761
+ for (const [path62, now] of next) {
26762
+ const before = previous.get(path62);
26740
26763
  if (!before) {
26741
- events.push({ path: path61, type: "create" });
26764
+ events.push({ path: path62, type: "create" });
26742
26765
  } else if (!now.isDir && (before.mtimeMs !== now.mtimeMs || before.size !== now.size)) {
26743
- events.push({ path: path61, type: "update" });
26766
+ events.push({ path: path62, type: "update" });
26744
26767
  }
26745
26768
  }
26746
- for (const path61 of previous.keys()) {
26747
- if (!next.has(path61)) events.push({ path: path61, type: "delete" });
26769
+ for (const path62 of previous.keys()) {
26770
+ if (!next.has(path62)) events.push({ path: path62, type: "delete" });
26748
26771
  }
26749
26772
  return events;
26750
26773
  }
@@ -27229,7 +27252,7 @@ async function parcelEventToFileEvent(event, rootInfo) {
27229
27252
  // ../tempo-sdk/package.json
27230
27253
  var package_default = {
27231
27254
  name: "tempo-sdk",
27232
- version: "0.0.37",
27255
+ version: "0.0.38",
27233
27256
  type: "module",
27234
27257
  description: "Tempo SDK \u2014 Vite, Next.js, and Expo plugins for JSX annotation and shared page/storyboard types",
27235
27258
  repository: {
@@ -33962,8 +33985,8 @@ function pathspec(...paths) {
33962
33985
  cache.set(key, paths);
33963
33986
  return key;
33964
33987
  }
33965
- function isPathSpec(path61) {
33966
- return path61 instanceof String && cache.has(path61);
33988
+ function isPathSpec(path62) {
33989
+ return path62 instanceof String && cache.has(path62);
33967
33990
  }
33968
33991
  function toPaths(pathSpec) {
33969
33992
  return cache.get(pathSpec) || [];
@@ -34052,8 +34075,8 @@ function toLinesWithContent(input = "", trimmed2 = true, separator = "\n") {
34052
34075
  function forEachLineWithContent(input, callback) {
34053
34076
  return toLinesWithContent(input, true).map((line) => callback(line));
34054
34077
  }
34055
- function folderExists(path61) {
34056
- return (0, import_file_exists.exists)(path61, import_file_exists.FOLDER);
34078
+ function folderExists(path62) {
34079
+ return (0, import_file_exists.exists)(path62, import_file_exists.FOLDER);
34057
34080
  }
34058
34081
  function append(target, item) {
34059
34082
  if (Array.isArray(target)) {
@@ -34457,8 +34480,8 @@ function checkIsRepoRootTask() {
34457
34480
  commands,
34458
34481
  format: "utf-8",
34459
34482
  onError,
34460
- parser(path61) {
34461
- return /^\.(git)?$/.test(path61.trim());
34483
+ parser(path62) {
34484
+ return /^\.(git)?$/.test(path62.trim());
34462
34485
  }
34463
34486
  };
34464
34487
  }
@@ -34892,11 +34915,11 @@ function parseGrep(grep) {
34892
34915
  const paths = /* @__PURE__ */ new Set();
34893
34916
  const results = {};
34894
34917
  forEachLineWithContent(grep, (input) => {
34895
- const [path61, line, preview] = input.split(NULL);
34896
- paths.add(path61);
34897
- (results[path61] = results[path61] || []).push({
34918
+ const [path62, line, preview] = input.split(NULL);
34919
+ paths.add(path62);
34920
+ (results[path62] = results[path62] || []).push({
34898
34921
  line: asNumber(line),
34899
- path: path61,
34922
+ path: path62,
34900
34923
  preview
34901
34924
  });
34902
34925
  });
@@ -35661,14 +35684,14 @@ var init_hash_object = __esm({
35661
35684
  init_task();
35662
35685
  }
35663
35686
  });
35664
- function parseInit(bare, path61, text) {
35687
+ function parseInit(bare, path62, text) {
35665
35688
  const response = String(text).trim();
35666
35689
  let result;
35667
35690
  if (result = initResponseRegex.exec(response)) {
35668
- return new InitSummary(bare, path61, false, result[1]);
35691
+ return new InitSummary(bare, path62, false, result[1]);
35669
35692
  }
35670
35693
  if (result = reInitResponseRegex.exec(response)) {
35671
- return new InitSummary(bare, path61, true, result[1]);
35694
+ return new InitSummary(bare, path62, true, result[1]);
35672
35695
  }
35673
35696
  let gitDir = "";
35674
35697
  const tokens = response.split(" ");
@@ -35679,7 +35702,7 @@ function parseInit(bare, path61, text) {
35679
35702
  break;
35680
35703
  }
35681
35704
  }
35682
- return new InitSummary(bare, path61, /^re/i.test(response), gitDir);
35705
+ return new InitSummary(bare, path62, /^re/i.test(response), gitDir);
35683
35706
  }
35684
35707
  var InitSummary;
35685
35708
  var initResponseRegex;
@@ -35688,9 +35711,9 @@ var init_InitSummary = __esm({
35688
35711
  "src/lib/responses/InitSummary.ts"() {
35689
35712
  "use strict";
35690
35713
  InitSummary = class {
35691
- constructor(bare, path61, existing, gitDir) {
35714
+ constructor(bare, path62, existing, gitDir) {
35692
35715
  this.bare = bare;
35693
- this.path = path61;
35716
+ this.path = path62;
35694
35717
  this.existing = existing;
35695
35718
  this.gitDir = gitDir;
35696
35719
  }
@@ -35702,7 +35725,7 @@ var init_InitSummary = __esm({
35702
35725
  function hasBareCommand(command) {
35703
35726
  return command.includes(bareCommand);
35704
35727
  }
35705
- function initTask(bare = false, path61, customArgs) {
35728
+ function initTask(bare = false, path62, customArgs) {
35706
35729
  const commands = ["init", ...customArgs];
35707
35730
  if (bare && !hasBareCommand(commands)) {
35708
35731
  commands.splice(1, 0, bareCommand);
@@ -35711,7 +35734,7 @@ function initTask(bare = false, path61, customArgs) {
35711
35734
  commands,
35712
35735
  format: "utf-8",
35713
35736
  parser(text) {
35714
- return parseInit(commands.includes("--bare"), path61, text);
35737
+ return parseInit(commands.includes("--bare"), path62, text);
35715
35738
  }
35716
35739
  };
35717
35740
  }
@@ -36527,12 +36550,12 @@ var init_FileStatusSummary = __esm({
36527
36550
  "use strict";
36528
36551
  fromPathRegex = /^(.+)\0(.+)$/;
36529
36552
  FileStatusSummary = class {
36530
- constructor(path61, index, working_dir) {
36531
- this.path = path61;
36553
+ constructor(path62, index, working_dir) {
36554
+ this.path = path62;
36532
36555
  this.index = index;
36533
36556
  this.working_dir = working_dir;
36534
36557
  if (index === "R" || working_dir === "R") {
36535
- const detail = fromPathRegex.exec(path61) || [null, path61, path61];
36558
+ const detail = fromPathRegex.exec(path62) || [null, path62, path62];
36536
36559
  this.from = detail[2] || "";
36537
36560
  this.path = detail[1] || "";
36538
36561
  }
@@ -36563,14 +36586,14 @@ function splitLine(result, lineStr) {
36563
36586
  default:
36564
36587
  return;
36565
36588
  }
36566
- function data(index, workingDir, path61) {
36589
+ function data(index, workingDir, path62) {
36567
36590
  const raw = `${index}${workingDir}`;
36568
36591
  const handler = parsers6.get(raw);
36569
36592
  if (handler) {
36570
- handler(result, path61);
36593
+ handler(result, path62);
36571
36594
  }
36572
36595
  if (raw !== "##" && raw !== "!!") {
36573
- result.files.push(new FileStatusSummary(path61, index, workingDir));
36596
+ result.files.push(new FileStatusSummary(path62, index, workingDir));
36574
36597
  }
36575
36598
  }
36576
36599
  }
@@ -36879,9 +36902,9 @@ var init_simple_git_api = __esm({
36879
36902
  next
36880
36903
  );
36881
36904
  }
36882
- hashObject(path61, write2) {
36905
+ hashObject(path62, write2) {
36883
36906
  return this._runTask(
36884
- hashObjectTask(path61, write2 === true),
36907
+ hashObjectTask(path62, write2 === true),
36885
36908
  trailingFunctionArgument(arguments)
36886
36909
  );
36887
36910
  }
@@ -37234,8 +37257,8 @@ var init_branch = __esm({
37234
37257
  }
37235
37258
  });
37236
37259
  function toPath(input) {
37237
- const path61 = input.trim().replace(/^["']|["']$/g, "");
37238
- return path61 && normalize(path61);
37260
+ const path62 = input.trim().replace(/^["']|["']$/g, "");
37261
+ return path62 && normalize(path62);
37239
37262
  }
37240
37263
  var parseCheckIgnore;
37241
37264
  var init_CheckIgnore = __esm({
@@ -37549,8 +37572,8 @@ __export(sub_module_exports, {
37549
37572
  subModuleTask: () => subModuleTask,
37550
37573
  updateSubModuleTask: () => updateSubModuleTask
37551
37574
  });
37552
- function addSubModuleTask(repo, path61) {
37553
- return subModuleTask(["add", repo, path61]);
37575
+ function addSubModuleTask(repo, path62) {
37576
+ return subModuleTask(["add", repo, path62]);
37554
37577
  }
37555
37578
  function initSubModuleTask(customArgs) {
37556
37579
  return subModuleTask(["init", ...customArgs]);
@@ -37883,8 +37906,8 @@ var require_git = __commonJS2({
37883
37906
  }
37884
37907
  return this._runTask(straightThroughStringTask2(command, this._trimmed), next);
37885
37908
  };
37886
- Git2.prototype.submoduleAdd = function(repo, path61, then) {
37887
- return this._runTask(addSubModuleTask2(repo, path61), trailingFunctionArgument2(arguments));
37909
+ Git2.prototype.submoduleAdd = function(repo, path62, then) {
37910
+ return this._runTask(addSubModuleTask2(repo, path62), trailingFunctionArgument2(arguments));
37888
37911
  };
37889
37912
  Git2.prototype.submoduleUpdate = function(args, then) {
37890
37913
  return this._runTask(
@@ -42412,17 +42435,17 @@ var APP_LAYOUT_CANDIDATES = [
42412
42435
  "src/app/layout.js"
42413
42436
  ];
42414
42437
  var PAGES_DIRECTORY_CANDIDATES = ["pages", "src/pages"];
42415
- async function fileExists3(path61) {
42438
+ async function fileExists3(path62) {
42416
42439
  try {
42417
- await access11(path61);
42440
+ await access11(path62);
42418
42441
  return true;
42419
42442
  } catch {
42420
42443
  return false;
42421
42444
  }
42422
42445
  }
42423
- async function directoryExists(path61) {
42446
+ async function directoryExists(path62) {
42424
42447
  try {
42425
- const stats = await stat6(path61);
42448
+ const stats = await stat6(path62);
42426
42449
  return stats.isDirectory();
42427
42450
  } catch {
42428
42451
  return false;
@@ -42559,9 +42582,9 @@ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
42559
42582
  ]);
42560
42583
  var DEFAULT_SCAN_ROOTS = ["apps", "packages"];
42561
42584
  var MAX_SCAN_DEPTH = 8;
42562
- async function pathExists9(path61) {
42585
+ async function pathExists9(path62) {
42563
42586
  try {
42564
- await access12(path61);
42587
+ await access12(path62);
42565
42588
  return true;
42566
42589
  } catch {
42567
42590
  return false;
@@ -42696,9 +42719,9 @@ var LOCK_FILE_PRIORITY = [
42696
42719
  packageManager: PackageManager.NPM
42697
42720
  }
42698
42721
  ];
42699
- async function fileExists4(path61) {
42722
+ async function fileExists4(path62) {
42700
42723
  try {
42701
- await access13(path61);
42724
+ await access13(path62);
42702
42725
  return true;
42703
42726
  } catch {
42704
42727
  return false;
@@ -42749,9 +42772,9 @@ var VITE_CONFIG_FILES = [
42749
42772
  "vite.config.mjs",
42750
42773
  "vite.config.mts"
42751
42774
  ];
42752
- async function fileExists5(path61) {
42775
+ async function fileExists5(path62) {
42753
42776
  try {
42754
- await access14(path61);
42777
+ await access14(path62);
42755
42778
  return true;
42756
42779
  } catch {
42757
42780
  return false;
@@ -45041,6 +45064,558 @@ async function runCanvasValidation(args, options) {
45041
45064
  };
45042
45065
  }
45043
45066
 
45067
+ // ../canvas-mcp/storyboard-stability.ts
45068
+ import { readFile as readFile24 } from "fs/promises";
45069
+ import path54 from "path";
45070
+
45071
+ // ../../node_modules/.pnpm/pixelmatch@7.1.0/node_modules/pixelmatch/index.js
45072
+ function pixelmatch(img1, img2, output, width, height, options = {}) {
45073
+ const {
45074
+ threshold = 0.1,
45075
+ alpha = 0.1,
45076
+ aaColor = [255, 255, 0],
45077
+ diffColor = [255, 0, 0],
45078
+ includeAA,
45079
+ diffColorAlt,
45080
+ diffMask
45081
+ } = options;
45082
+ if (!isPixelData(img1) || !isPixelData(img2) || output && !isPixelData(output))
45083
+ throw new Error("Image data: Uint8Array, Uint8ClampedArray or Buffer expected.");
45084
+ if (img1.length !== img2.length || output && output.length !== img1.length)
45085
+ throw new Error("Image sizes do not match.");
45086
+ if (img1.length !== width * height * 4) throw new Error("Image data size does not match width/height.");
45087
+ const len = width * height;
45088
+ const a32 = new Uint32Array(img1.buffer, img1.byteOffset, len);
45089
+ const b32 = new Uint32Array(img2.buffer, img2.byteOffset, len);
45090
+ let identical = true;
45091
+ for (let i = 0; i < len; i++) {
45092
+ if (a32[i] !== b32[i]) {
45093
+ identical = false;
45094
+ break;
45095
+ }
45096
+ }
45097
+ if (identical) {
45098
+ if (output && !diffMask) {
45099
+ for (let i = 0; i < len; i++) drawGrayPixel(img1, 4 * i, alpha, output);
45100
+ }
45101
+ return 0;
45102
+ }
45103
+ const maxDelta = 35215 * threshold * threshold;
45104
+ const [aaR, aaG, aaB] = aaColor;
45105
+ const [diffR, diffG, diffB] = diffColor;
45106
+ const [altR, altG, altB] = diffColorAlt || diffColor;
45107
+ let diff = 0;
45108
+ for (let y = 0; y < height; y++) {
45109
+ for (let x = 0; x < width; x++) {
45110
+ const i = y * width + x;
45111
+ const pos = i * 4;
45112
+ const delta = a32[i] === b32[i] ? 0 : colorDelta(img1, img2, pos, pos, false);
45113
+ if (Math.abs(delta) > maxDelta) {
45114
+ const isAA = antialiased(img1, x, y, width, height, a32, b32) || antialiased(img2, x, y, width, height, b32, a32);
45115
+ if (!includeAA && isAA) {
45116
+ if (output && !diffMask) drawPixel(output, pos, aaR, aaG, aaB);
45117
+ } else {
45118
+ if (output) {
45119
+ if (delta < 0) {
45120
+ drawPixel(output, pos, altR, altG, altB);
45121
+ } else {
45122
+ drawPixel(output, pos, diffR, diffG, diffB);
45123
+ }
45124
+ }
45125
+ diff++;
45126
+ }
45127
+ } else if (output && !diffMask) {
45128
+ drawGrayPixel(img1, pos, alpha, output);
45129
+ }
45130
+ }
45131
+ }
45132
+ return diff;
45133
+ }
45134
+ function isPixelData(arr) {
45135
+ return ArrayBuffer.isView(arr) && arr.BYTES_PER_ELEMENT === 1;
45136
+ }
45137
+ function antialiased(img, x1, y1, width, height, a32, b32) {
45138
+ const x0 = Math.max(x1 - 1, 0);
45139
+ const y0 = Math.max(y1 - 1, 0);
45140
+ const x2 = Math.min(x1 + 1, width - 1);
45141
+ const y2 = Math.min(y1 + 1, height - 1);
45142
+ const pos = y1 * width + x1;
45143
+ let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0;
45144
+ let min = 0;
45145
+ let max = 0;
45146
+ let minX = 0;
45147
+ let minY = 0;
45148
+ let maxX = 0;
45149
+ let maxY = 0;
45150
+ for (let x = x0; x <= x2; x++) {
45151
+ for (let y = y0; y <= y2; y++) {
45152
+ if (x === x1 && y === y1) continue;
45153
+ const delta = colorDelta(img, img, pos * 4, (y * width + x) * 4, true);
45154
+ if (delta === 0) {
45155
+ zeroes++;
45156
+ if (zeroes > 2) return false;
45157
+ } else if (delta < min) {
45158
+ min = delta;
45159
+ minX = x;
45160
+ minY = y;
45161
+ } else if (delta > max) {
45162
+ max = delta;
45163
+ maxX = x;
45164
+ maxY = y;
45165
+ }
45166
+ }
45167
+ }
45168
+ if (min === 0 || max === 0) return false;
45169
+ return hasManySiblings(a32, minX, minY, width, height) && hasManySiblings(b32, minX, minY, width, height) || hasManySiblings(a32, maxX, maxY, width, height) && hasManySiblings(b32, maxX, maxY, width, height);
45170
+ }
45171
+ function hasManySiblings(img, x1, y1, width, height) {
45172
+ const x0 = Math.max(x1 - 1, 0);
45173
+ const y0 = Math.max(y1 - 1, 0);
45174
+ const x2 = Math.min(x1 + 1, width - 1);
45175
+ const y2 = Math.min(y1 + 1, height - 1);
45176
+ const val = img[y1 * width + x1];
45177
+ let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0;
45178
+ for (let x = x0; x <= x2; x++) {
45179
+ for (let y = y0; y <= y2; y++) {
45180
+ if (x === x1 && y === y1) continue;
45181
+ zeroes += +(val === img[y * width + x]);
45182
+ if (zeroes > 2) return true;
45183
+ }
45184
+ }
45185
+ return false;
45186
+ }
45187
+ function colorDelta(img1, img2, k2, m, yOnly) {
45188
+ const r1 = img1[k2];
45189
+ const g1 = img1[k2 + 1];
45190
+ const b1 = img1[k2 + 2];
45191
+ const a1 = img1[k2 + 3];
45192
+ const r2 = img2[m];
45193
+ const g2 = img2[m + 1];
45194
+ const b2 = img2[m + 2];
45195
+ const a2 = img2[m + 3];
45196
+ let dr = r1 - r2;
45197
+ let dg = g1 - g2;
45198
+ let db = b1 - b2;
45199
+ const da = a1 - a2;
45200
+ if (!dr && !dg && !db && !da) return 0;
45201
+ if (a1 < 255 || a2 < 255) {
45202
+ const rb = 48 + 159 * (k2 % 2);
45203
+ const gb = 48 + 159 * ((k2 / 1.618033988749895 | 0) % 2);
45204
+ const bb = 48 + 159 * ((k2 / 2.618033988749895 | 0) % 2);
45205
+ dr = (r1 * a1 - r2 * a2 - rb * da) / 255;
45206
+ dg = (g1 * a1 - g2 * a2 - gb * da) / 255;
45207
+ db = (b1 * a1 - b2 * a2 - bb * da) / 255;
45208
+ }
45209
+ const y = dr * 0.29889531 + dg * 0.58662247 + db * 0.11448223;
45210
+ if (yOnly) return y;
45211
+ const i = dr * 0.59597799 - dg * 0.2741761 - db * 0.32180189;
45212
+ const q = dr * 0.21147017 - dg * 0.52261711 + db * 0.31114694;
45213
+ const delta = 0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q;
45214
+ return y > 0 ? -delta : delta;
45215
+ }
45216
+ function drawPixel(output, pos, r, g, b2) {
45217
+ output[pos + 0] = r;
45218
+ output[pos + 1] = g;
45219
+ output[pos + 2] = b2;
45220
+ output[pos + 3] = 255;
45221
+ }
45222
+ function drawGrayPixel(img, i, alpha, output) {
45223
+ const val = 255 + (img[i] * 0.29889531 + img[i + 1] * 0.58662247 + img[i + 2] * 0.11448223 - 255) * alpha * img[i + 3] / 255;
45224
+ drawPixel(output, i, val, val, val);
45225
+ }
45226
+
45227
+ // ../canvas-mcp/storyboard-stability.ts
45228
+ var import_pngjs = __toESM(require_png(), 1);
45229
+ var DEFAULT_VRT_PIXEL_THRESHOLD = 0.1;
45230
+ var MAX_STABILITY_DIFF_DIMENSION_PX = 1990;
45231
+ var StoryboardStabilityTargetError = class extends Error {
45232
+ constructor(code, message) {
45233
+ super(message);
45234
+ this.name = "StoryboardStabilityTargetError";
45235
+ this.code = code;
45236
+ }
45237
+ };
45238
+ function toPosix(value) {
45239
+ return value.replace(/\\/g, "/");
45240
+ }
45241
+ function absoluteCanvasesRoot(workspaceRoot, canvasesDir) {
45242
+ return path54.isAbsolute(canvasesDir) ? path54.resolve(canvasesDir) : path54.resolve(workspaceRoot, canvasesDir);
45243
+ }
45244
+ async function resolveCanvasFile(options) {
45245
+ const canvasesRoot = absoluteCanvasesRoot(
45246
+ options.workspaceRoot,
45247
+ options.canvasesDir
45248
+ );
45249
+ const identifier = options.canvasSlug;
45250
+ const resolved = /* @__PURE__ */ new Set();
45251
+ const direct = await resolveCanvasFilePath(
45252
+ path54.join(canvasesRoot, identifier)
45253
+ );
45254
+ if (direct) resolved.add(direct);
45255
+ if (resolved.size === 0 && !identifier.includes("/")) {
45256
+ const files = await findPageFilesInPagesRoot(canvasesRoot);
45257
+ for (const file of files) {
45258
+ if (path54.basename(path54.dirname(file)) === identifier) resolved.add(file);
45259
+ }
45260
+ }
45261
+ if (resolved.size === 0) {
45262
+ throw new StoryboardStabilityTargetError(
45263
+ "canvas_not_found",
45264
+ `No canvas matching "${options.canvasSlug}" exists under ${toPosix(canvasesRoot)}.`
45265
+ );
45266
+ }
45267
+ if (resolved.size > 1) {
45268
+ const candidates = [...resolved].map((file) => toPosix(path54.relative(canvasesRoot, path54.dirname(file)))).sort();
45269
+ throw new StoryboardStabilityTargetError(
45270
+ "ambiguous_canvas_slug",
45271
+ `"${options.canvasSlug}" names ${candidates.length} canvases: ${candidates.join(", ")}. Pass the full canvases-root-relative path.`
45272
+ );
45273
+ }
45274
+ const canvasFile = resolved.values().next().value;
45275
+ if (!canvasFile) {
45276
+ throw new StoryboardStabilityTargetError(
45277
+ "canvas_not_found",
45278
+ `No canvas matching "${options.canvasSlug}" exists under ${toPosix(canvasesRoot)}.`
45279
+ );
45280
+ }
45281
+ return canvasFile;
45282
+ }
45283
+ async function resolveTestedStoryboard(options) {
45284
+ const canvasFile = await resolveCanvasFile(options);
45285
+ const source = await readFile24(canvasFile, "utf8");
45286
+ const parsed = parsePageSource(canvasFile, source);
45287
+ const storyboard = parsed.storyboards.find(
45288
+ (candidate) => candidate.storyboardKey === options.storyboard || candidate.exportName === options.storyboard
45289
+ );
45290
+ if (!storyboard) {
45291
+ const available = parsed.storyboards.map((candidate) => candidate.storyboardKey ?? candidate.exportName).join(", ");
45292
+ throw new StoryboardStabilityTargetError(
45293
+ "storyboard_not_found",
45294
+ `Storyboard "${options.storyboard}" was not found in ${toPosix(path54.relative(options.workspaceRoot, canvasFile))}.${available ? ` Available storyboards: ${available}.` : ""}`
45295
+ );
45296
+ }
45297
+ if (storyboard.error) {
45298
+ throw new StoryboardStabilityTargetError(
45299
+ "storyboard_invalid",
45300
+ `Storyboard "${options.storyboard}" cannot be verified: ${storyboard.error}`
45301
+ );
45302
+ }
45303
+ if (storyboard.isRoute || storyboard.isHtml) {
45304
+ throw new StoryboardStabilityTargetError(
45305
+ "storyboard_unsupported",
45306
+ `Storyboard "${options.storyboard}" is not a component storyboard; VRT stability verification currently supports component storyboards only.`
45307
+ );
45308
+ }
45309
+ if (!storyboard.vrt || !storyboard.vrtConfig) {
45310
+ throw new StoryboardStabilityTargetError(
45311
+ "storyboard_not_tested",
45312
+ `Storyboard "${options.storyboard}" is not VRT-tested. Add a literal vrt prop before verifying stability.`
45313
+ );
45314
+ }
45315
+ if (storyboard.vrtConfig.skip) {
45316
+ throw new StoryboardStabilityTargetError(
45317
+ "storyboard_paused",
45318
+ `Storyboard "${options.storyboard}" is paused with vrt={{ skip: true }}. Remove skip before verifying it as an active test.`
45319
+ );
45320
+ }
45321
+ const maskedArea = (storyboard.vrtConfig.ignore ?? []).reduce(
45322
+ (sum, region) => sum + region.w * region.h,
45323
+ 0
45324
+ );
45325
+ if (maskedArea >= 0.9 * storyboard.layout.width * storyboard.layout.height) {
45326
+ throw new StoryboardStabilityTargetError(
45327
+ "storyboard_invalid",
45328
+ `Storyboard "${options.storyboard}" has vrt.ignore masking at least 90% of its area; VRT rejects this configuration.`
45329
+ );
45330
+ }
45331
+ return {
45332
+ canvasFile: toPosix(path54.relative(options.workspaceRoot, canvasFile)),
45333
+ canvasSlug: toPosix(
45334
+ path54.relative(
45335
+ absoluteCanvasesRoot(options.workspaceRoot, options.canvasesDir),
45336
+ path54.dirname(canvasFile)
45337
+ )
45338
+ ),
45339
+ storyboardId: storyboard.storyboardKey ?? storyboard.exportName,
45340
+ layout: {
45341
+ width: storyboard.layout.width,
45342
+ height: storyboard.layout.height
45343
+ },
45344
+ vrt: storyboard.vrtConfig
45345
+ };
45346
+ }
45347
+ function maskRegion(image, region) {
45348
+ const x0 = Math.max(0, Math.floor(region.x));
45349
+ const y0 = Math.max(0, Math.floor(region.y));
45350
+ const x1 = Math.min(image.width, Math.ceil(region.x + region.w));
45351
+ const y1 = Math.min(image.height, Math.ceil(region.y + region.h));
45352
+ for (let y = y0; y < y1; y += 1) {
45353
+ for (let x = x0; x < x1; x += 1) {
45354
+ const offset = image.width * y + x << 2;
45355
+ image.data[offset] = 0;
45356
+ image.data[offset + 1] = 0;
45357
+ image.data[offset + 2] = 0;
45358
+ image.data[offset + 3] = 255;
45359
+ }
45360
+ }
45361
+ }
45362
+ function findDiffBounds(diff) {
45363
+ let minX = diff.width;
45364
+ let minY = diff.height;
45365
+ let maxX = -1;
45366
+ let maxY = -1;
45367
+ for (let y = 0; y < diff.height; y += 1) {
45368
+ for (let x = 0; x < diff.width; x += 1) {
45369
+ if (diff.data[(diff.width * y + x << 2) + 3] === 0) continue;
45370
+ minX = Math.min(minX, x);
45371
+ minY = Math.min(minY, y);
45372
+ maxX = Math.max(maxX, x);
45373
+ maxY = Math.max(maxY, y);
45374
+ }
45375
+ }
45376
+ return maxX < minX || maxY < minY ? null : {
45377
+ x: minX,
45378
+ y: minY,
45379
+ width: maxX - minX + 1,
45380
+ height: maxY - minY + 1
45381
+ };
45382
+ }
45383
+ function unionBounds(left, right) {
45384
+ if (!left) return right;
45385
+ if (!right) return left;
45386
+ const x = Math.min(left.x, right.x);
45387
+ const y = Math.min(left.y, right.y);
45388
+ const x1 = Math.max(left.x + left.width, right.x + right.width);
45389
+ const y1 = Math.max(left.y + left.height, right.y + right.height);
45390
+ return { x, y, width: x1 - x, height: y1 - y };
45391
+ }
45392
+ function decodeCapture(capture) {
45393
+ const image = import_pngjs.PNG.sync.read(Buffer.from(capture.pngBase64, "base64"));
45394
+ if (image.width !== capture.width || image.height !== capture.height) {
45395
+ throw new Error(
45396
+ `Capture metadata ${capture.width}x${capture.height} does not match decoded PNG ${image.width}x${image.height}.`
45397
+ );
45398
+ }
45399
+ return image;
45400
+ }
45401
+ function compareCaptureToBaseline(baseline, capture, run, options) {
45402
+ const candidate = decodeCapture(capture);
45403
+ if (candidate.width !== baseline.width || candidate.height !== baseline.height) {
45404
+ const width = Math.max(baseline.width, candidate.width);
45405
+ const height = Math.max(baseline.height, candidate.height);
45406
+ const diff2 = new import_pngjs.PNG({ width, height });
45407
+ for (let offset = 0; offset < diff2.data.length; offset += 4) {
45408
+ diff2.data[offset] = 217;
45409
+ diff2.data[offset + 1] = 70;
45410
+ diff2.data[offset + 2] = 239;
45411
+ diff2.data[offset + 3] = 255;
45412
+ }
45413
+ return {
45414
+ diff: diff2,
45415
+ comparison: {
45416
+ run,
45417
+ baselineRun: 1,
45418
+ diffPixels: candidate.width * candidate.height,
45419
+ diffRatio: 1,
45420
+ diffBounds: { x: 0, y: 0, width, height },
45421
+ dimensions: { width: candidate.width, height: candidate.height },
45422
+ dimensionChange: {
45423
+ baseline: { width: baseline.width, height: baseline.height },
45424
+ candidate: { width: candidate.width, height: candidate.height }
45425
+ }
45426
+ }
45427
+ };
45428
+ }
45429
+ const scaleX = baseline.width / options.layout.width;
45430
+ const scaleY = baseline.height / options.layout.height;
45431
+ for (const region of options.ignore) {
45432
+ const scaled = {
45433
+ x: region.x * scaleX,
45434
+ y: region.y * scaleY,
45435
+ w: region.w * scaleX,
45436
+ h: region.h * scaleY
45437
+ };
45438
+ maskRegion(candidate, scaled);
45439
+ }
45440
+ const diff = new import_pngjs.PNG({ width: baseline.width, height: baseline.height });
45441
+ const diffPixels = pixelmatch(
45442
+ baseline.data,
45443
+ candidate.data,
45444
+ diff.data,
45445
+ baseline.width,
45446
+ baseline.height,
45447
+ {
45448
+ threshold: options.threshold,
45449
+ includeAA: false,
45450
+ diffMask: true,
45451
+ diffColor: [217, 70, 239]
45452
+ }
45453
+ );
45454
+ return {
45455
+ diff,
45456
+ comparison: {
45457
+ run,
45458
+ baselineRun: 1,
45459
+ diffPixels,
45460
+ diffRatio: diffPixels / (baseline.width * baseline.height),
45461
+ diffBounds: findDiffBounds(diff),
45462
+ dimensions: { width: candidate.width, height: candidate.height }
45463
+ }
45464
+ };
45465
+ }
45466
+ function mergeDiffImage(current, next) {
45467
+ const width = Math.max(current?.width ?? 0, next.width);
45468
+ const height = Math.max(current?.height ?? 0, next.height);
45469
+ const merged = current && current.width === width && current.height === height ? current : new import_pngjs.PNG({ width, height });
45470
+ const merge = (source) => {
45471
+ for (let y = 0; y < source.height; y += 1) {
45472
+ for (let x = 0; x < source.width; x += 1) {
45473
+ const sourceOffset = source.width * y + x << 2;
45474
+ if (source.data[sourceOffset + 3] === 0) continue;
45475
+ const targetOffset = width * y + x << 2;
45476
+ merged.data[targetOffset] = 217;
45477
+ merged.data[targetOffset + 1] = 70;
45478
+ merged.data[targetOffset + 2] = 239;
45479
+ merged.data[targetOffset + 3] = 255;
45480
+ }
45481
+ }
45482
+ };
45483
+ if (current && merged !== current) merge(current);
45484
+ merge(next);
45485
+ return merged;
45486
+ }
45487
+ function downscaleDiffMaskToFit(image, maxDimension) {
45488
+ if (image.width <= maxDimension && image.height <= maxDimension) return image;
45489
+ const scale = Math.min(
45490
+ maxDimension / image.width,
45491
+ maxDimension / image.height
45492
+ );
45493
+ const width = Math.max(
45494
+ 1,
45495
+ Math.min(maxDimension, Math.round(image.width * scale))
45496
+ );
45497
+ const height = Math.max(
45498
+ 1,
45499
+ Math.min(maxDimension, Math.round(image.height * scale))
45500
+ );
45501
+ const output = new import_pngjs.PNG({ width, height });
45502
+ for (let y = 0; y < height; y += 1) {
45503
+ const sourceY0 = Math.floor(y * image.height / height);
45504
+ const sourceY1 = Math.max(
45505
+ sourceY0 + 1,
45506
+ Math.ceil((y + 1) * image.height / height)
45507
+ );
45508
+ for (let x = 0; x < width; x += 1) {
45509
+ const sourceX0 = Math.floor(x * image.width / width);
45510
+ const sourceX1 = Math.max(
45511
+ sourceX0 + 1,
45512
+ Math.ceil((x + 1) * image.width / width)
45513
+ );
45514
+ let changed = false;
45515
+ for (let sourceY = sourceY0; sourceY < sourceY1 && !changed; sourceY += 1) {
45516
+ for (let sourceX = sourceX0; sourceX < sourceX1; sourceX += 1) {
45517
+ const sourceOffset = image.width * sourceY + sourceX << 2;
45518
+ if (image.data[sourceOffset + 3] !== 0) {
45519
+ changed = true;
45520
+ break;
45521
+ }
45522
+ }
45523
+ }
45524
+ if (!changed) continue;
45525
+ const outputOffset = width * y + x << 2;
45526
+ output.data[outputOffset] = 217;
45527
+ output.data[outputOffset + 1] = 70;
45528
+ output.data[outputOffset + 2] = 239;
45529
+ output.data[outputOffset + 3] = 255;
45530
+ }
45531
+ }
45532
+ return output;
45533
+ }
45534
+ function createVrtStabilityAccumulator(options) {
45535
+ const threshold = options.threshold ?? DEFAULT_VRT_PIXEL_THRESHOLD;
45536
+ const compareOptions = {
45537
+ threshold,
45538
+ ignore: options.ignore ?? [],
45539
+ layout: options.layout
45540
+ };
45541
+ let baseline = null;
45542
+ let cumulativeDiff = null;
45543
+ let runs = 0;
45544
+ let retriedCaptures = 0;
45545
+ const comparisons = [];
45546
+ return {
45547
+ addCapture(capture) {
45548
+ runs += 1;
45549
+ if (capture.retried === true) retriedCaptures += 1;
45550
+ if (!baseline) {
45551
+ baseline = decodeCapture(capture);
45552
+ const scaleX = baseline.width / compareOptions.layout.width;
45553
+ const scaleY = baseline.height / compareOptions.layout.height;
45554
+ for (const region of compareOptions.ignore) {
45555
+ maskRegion(baseline, {
45556
+ x: region.x * scaleX,
45557
+ y: region.y * scaleY,
45558
+ w: region.w * scaleX,
45559
+ h: region.h * scaleY
45560
+ });
45561
+ }
45562
+ return;
45563
+ }
45564
+ const result = compareCaptureToBaseline(
45565
+ baseline,
45566
+ capture,
45567
+ runs,
45568
+ compareOptions
45569
+ );
45570
+ comparisons.push(result.comparison);
45571
+ if (result.comparison.diffPixels > 0) {
45572
+ cumulativeDiff = mergeDiffImage(cumulativeDiff, result.diff);
45573
+ }
45574
+ },
45575
+ get captureCount() {
45576
+ return runs;
45577
+ },
45578
+ summarize() {
45579
+ if (!baseline || runs < 2) {
45580
+ throw new Error(
45581
+ "VRT stability verification requires at least two captures."
45582
+ );
45583
+ }
45584
+ const changed = comparisons.filter(
45585
+ (comparison) => comparison.diffPixels > 0
45586
+ );
45587
+ const mergedDiff = cumulativeDiff;
45588
+ const outputDiff = mergedDiff ? downscaleDiffMaskToFit(mergedDiff, MAX_STABILITY_DIFF_DIMENSION_PX) : null;
45589
+ return {
45590
+ status: changed.length === 0 ? "stable" : "unstable",
45591
+ vrtStable: changed.length === 0,
45592
+ runs,
45593
+ retriedCaptures,
45594
+ threshold,
45595
+ ignoreRegions: compareOptions.ignore.length,
45596
+ changedRuns: changed.length,
45597
+ maxDiffPixels: Math.max(
45598
+ 0,
45599
+ ...comparisons.map((item) => item.diffPixels)
45600
+ ),
45601
+ maxDiffRatio: Math.max(0, ...comparisons.map((item) => item.diffRatio)),
45602
+ diffBounds: comparisons.reduce(
45603
+ (bounds, comparison) => unionBounds(bounds, comparison.diffBounds),
45604
+ null
45605
+ ),
45606
+ cumulativeDiff: outputDiff && mergedDiff ? {
45607
+ pngBase64: import_pngjs.PNG.sync.write(outputDiff).toString("base64"),
45608
+ width: outputDiff.width,
45609
+ height: outputDiff.height,
45610
+ sourceWidth: mergedDiff.width,
45611
+ sourceHeight: mergedDiff.height
45612
+ } : null,
45613
+ comparisons
45614
+ };
45615
+ }
45616
+ };
45617
+ }
45618
+
45044
45619
  // ../canvas-mcp/tools.ts
45045
45620
  function textResult2(text) {
45046
45621
  return { content: [{ type: "text", text }] };
@@ -45052,12 +45627,12 @@ async function normalizeCanvasSlug(input, workspaceRoot) {
45052
45627
  const value = input.trim().replace(/\\/g, "/").replace(/\/+$/, "").replace(/\/[^/]*\.canvas\.tsx$/i, "");
45053
45628
  if (!value.includes("/")) return value;
45054
45629
  const canvasesDir = resolveCanvasesDirSync(workspaceRoot);
45055
- const absoluteCanvasesDir = path54.isAbsolute(canvasesDir) ? canvasesDir : path54.join(workspaceRoot, canvasesDir);
45056
- const candidates = path54.isAbsolute(value) ? [value] : [path54.join(workspaceRoot, value), path54.join(absoluteCanvasesDir, value)];
45630
+ const absoluteCanvasesDir = path55.isAbsolute(canvasesDir) ? canvasesDir : path55.join(workspaceRoot, canvasesDir);
45631
+ const candidates = path55.isAbsolute(value) ? [value] : [path55.join(workspaceRoot, value), path55.join(absoluteCanvasesDir, value)];
45057
45632
  for (const candidate of candidates) {
45058
45633
  const dir = await resolveCanvasDirPath(candidate);
45059
45634
  if (!dir) continue;
45060
- const relative3 = path54.relative(absoluteCanvasesDir, dir).replace(/\\/g, "/");
45635
+ const relative3 = path55.relative(absoluteCanvasesDir, dir).replace(/\\/g, "/");
45061
45636
  return relative3 && !relative3.startsWith("..") ? relative3 : value;
45062
45637
  }
45063
45638
  return value;
@@ -45793,6 +46368,152 @@ function registerTools(server, context) {
45793
46368
  signal: extra.signal
45794
46369
  })
45795
46370
  );
46371
+ const stabilityErrorResult = (code, message) => errorResult9(`canvas_verify_vrt failed [${code}]: ${message}`);
46372
+ server.tool(
46373
+ "canvas_verify_vrt",
46374
+ "Check that a VRT-tracked storyboard renders identically across fresh mounts so its baseline won't flake in CI. Args: canvas_slug (folder name, or root-relative path when the name isn't unique), storyboard (export name carrying a literal `vrt` prop; `skip: true` is rejected), runs (2\u201310, default 3). Compares each run to run 1 under the visual-review matcher, threshold, and ignore masks. Returns `vrt_stable`; when false, also changed-pixel bounds and a diff-mask image. Call after adding or changing `vrt`; fix the nondeterministic input and rerun. Read-only. Fails fast with capacity_exceeded; typed errors are not retryable.",
46375
+ {
46376
+ canvas_slug: screenshotSchema.canvas_slug,
46377
+ storyboard: z24.string().describe("Storyboard export name carrying the literal `vrt` prop."),
46378
+ runs: z24.number().int().min(2).max(10).default(3).describe("Number of captures to compare. Defaults to 3.")
46379
+ },
46380
+ async ({ canvas_slug, storyboard, runs }, extra) => {
46381
+ if (!context.canvasStoryboardStability)
46382
+ return unsupportedEnvironmentResult("canvasStoryboardStability");
46383
+ try {
46384
+ const slug = await normalizeCanvasSlug(
46385
+ canvas_slug,
46386
+ context.workspaceRoot
46387
+ );
46388
+ const target = await resolveTestedStoryboard({
46389
+ workspaceRoot: context.workspaceRoot,
46390
+ canvasesDir: context.canvasesDir,
46391
+ canvasSlug: slug,
46392
+ storyboard
46393
+ });
46394
+ if (context.canvasDevserver && !context.canvasStoryboardStability.managesDevserver) {
46395
+ try {
46396
+ await context.canvasDevserver.start();
46397
+ } catch (startError) {
46398
+ return errorResult9(
46399
+ `canvas_verify_vrt could not start the canvas devserver: ${startError instanceof Error ? startError.message : String(startError)}`
46400
+ );
46401
+ }
46402
+ }
46403
+ const accumulator = createVrtStabilityAccumulator({
46404
+ threshold: target.vrt.threshold,
46405
+ ignore: target.vrt.ignore,
46406
+ layout: target.layout
46407
+ });
46408
+ let comparisonError;
46409
+ const result = await context.canvasStoryboardStability.captureRuns(
46410
+ {
46411
+ canvasSlug: target.canvasSlug,
46412
+ canvasFile: target.canvasFile,
46413
+ storyboard: target.storyboardId,
46414
+ runs
46415
+ },
46416
+ {
46417
+ timeoutMs: 12e4,
46418
+ signal: extra.signal,
46419
+ onCapture: (capture) => {
46420
+ if (comparisonError) return;
46421
+ try {
46422
+ accumulator.addCapture(capture);
46423
+ } catch (error) {
46424
+ comparisonError = error;
46425
+ }
46426
+ }
46427
+ }
46428
+ );
46429
+ if (comparisonError) {
46430
+ return stabilityErrorResult(
46431
+ "compare_failed",
46432
+ comparisonError instanceof Error ? comparisonError.message : String(comparisonError)
46433
+ );
46434
+ }
46435
+ if (result.kind === "error") {
46436
+ return stabilityErrorResult(
46437
+ result.code,
46438
+ `${result.message}${result.availableStoryboards && result.availableStoryboards.length > 0 ? ` Available storyboards in the current canvas: ${result.availableStoryboards.join(", ")}.` : ""}`
46439
+ );
46440
+ }
46441
+ if (result.runs !== runs || accumulator.captureCount !== runs) {
46442
+ return stabilityErrorResult(
46443
+ "capture_incomplete",
46444
+ `requested ${runs} fresh captures but received ${accumulator.captureCount}.`
46445
+ );
46446
+ }
46447
+ let summary;
46448
+ try {
46449
+ summary = accumulator.summarize();
46450
+ } catch (error) {
46451
+ return stabilityErrorResult(
46452
+ "compare_failed",
46453
+ error instanceof Error ? error.message : String(error)
46454
+ );
46455
+ }
46456
+ const diagnosis = summary.status === "stable" ? `All ${runs} isolated renders are equivalent under the storyboard's VRT threshold and ignore masks.` : `${summary.changedRuns} of ${runs - 1} comparisons changed under the storyboard's VRT policy; the largest changed ${summary.maxDiffPixels} pixels (${(summary.maxDiffRatio * 100).toFixed(4)}%).`;
46457
+ const content = [];
46458
+ if (summary.cumulativeDiff) {
46459
+ content.push({
46460
+ type: "image",
46461
+ data: summary.cumulativeDiff.pngBase64,
46462
+ mimeType: "image/png"
46463
+ });
46464
+ }
46465
+ content.push({
46466
+ type: "text",
46467
+ text: JSON.stringify(
46468
+ {
46469
+ canvas_slug: target.canvasSlug,
46470
+ canvas_file: target.canvasFile,
46471
+ storyboard: target.storyboardId,
46472
+ vrt: target.vrt,
46473
+ status: summary.status,
46474
+ vrt_stable: summary.vrtStable,
46475
+ runs: summary.runs,
46476
+ retried_captures: summary.retriedCaptures,
46477
+ threshold_used: summary.threshold,
46478
+ ignore_regions: summary.ignoreRegions,
46479
+ changed_runs: summary.changedRuns,
46480
+ max_diff_pixels: summary.maxDiffPixels,
46481
+ max_diff_ratio: summary.maxDiffRatio,
46482
+ diff_bounds_device_px: summary.diffBounds,
46483
+ cumulative_diff_image: summary.cumulativeDiff ? {
46484
+ width: summary.cumulativeDiff.width,
46485
+ height: summary.cumulativeDiff.height,
46486
+ source_width: summary.cumulativeDiff.sourceWidth,
46487
+ source_height: summary.cumulativeDiff.sourceHeight,
46488
+ meaning: "Transparent pixels were stable; fuchsia pixels changed in at least one comparison to run 1. Large masks are area-resampled to fit within 1990px without dropping thin differences."
46489
+ } : null,
46490
+ comparisons: summary.comparisons.map((comparison) => ({
46491
+ run: comparison.run,
46492
+ baseline_run: comparison.baselineRun,
46493
+ diff_pixels: comparison.diffPixels,
46494
+ diff_ratio: comparison.diffRatio,
46495
+ diff_bounds_device_px: comparison.diffBounds,
46496
+ dimensions: comparison.dimensions,
46497
+ ...comparison.dimensionChange ? { dimension_change: comparison.dimensionChange } : {}
46498
+ })),
46499
+ diagnosis,
46500
+ next_action: summary.status === "unstable" ? "Inspect the changed region and the storyboard's component dependency graph for nondeterministic render inputs. Replace them with fixed props, fixtures, or seeded inputs, then rerun." : "No deterministic-source change is needed for this storyboard under its current VRT policy."
46501
+ },
46502
+ null,
46503
+ 2
46504
+ )
46505
+ });
46506
+ return { content };
46507
+ } catch (error) {
46508
+ if (error instanceof StoryboardStabilityTargetError) {
46509
+ return stabilityErrorResult(error.code, error.message);
46510
+ }
46511
+ return errorResult9(
46512
+ `canvas_verify_vrt failed: ${error instanceof Error ? error.message : String(error)}`
46513
+ );
46514
+ }
46515
+ }
46516
+ );
45796
46517
  server.tool(
45797
46518
  "canvas_share",
45798
46519
  // The implementation lives in `tempo-client/electron/src/main/adapters/
@@ -45979,26 +46700,20 @@ function buildBackends(runtime, toolsets, workspace) {
45979
46700
  }
45980
46701
 
45981
46702
  // src/version.ts
45982
- var CLI_VERSION = "0.0.105";
46703
+ var CLI_VERSION = "0.0.106";
45983
46704
 
45984
46705
  // src/canvas-hooks.ts
45985
- var import_pngjs3 = __toESM(require_png(), 1);
45986
- import path60 from "path";
46706
+ var import_pngjs4 = __toESM(require_png(), 1);
46707
+ import path61 from "path";
45987
46708
  import { stat as stat10 } from "fs/promises";
45988
46709
  import { gzipSync } from "zlib";
45989
46710
  import { anyApi as anyApi9 } from "convex/server";
45990
46711
 
45991
46712
  // ../canvas-capture-headless/src/capture.ts
45992
- var import_pngjs = __toESM(require_png(), 1);
46713
+ var import_pngjs2 = __toESM(require_png(), 1);
45993
46714
  import { createHash as createHash4 } from "crypto";
45994
46715
 
45995
- // ../canvas-capture-headless/src/determinism.ts
45996
- var CHROMIUM_LAUNCH_ARGS = [
45997
- "--force-color-profile=srgb",
45998
- "--force-device-scale-factor=2",
45999
- "--hide-scrollbars",
46000
- "--disable-lcd-text"
46001
- ];
46716
+ // ../../modules/frame-pool/capture-determinism.ts
46002
46717
  var FROZEN_EPOCH_MS = Date.UTC(2026, 0, 1, 12, 0, 0);
46003
46718
  function buildFreezeInitScript() {
46004
46719
  return `(() => {
@@ -46018,7 +46733,6 @@ function buildFreezeInitScript() {
46018
46733
  }
46019
46734
  FrozenDate.parse = RealDate.parse;
46020
46735
  FrozenDate.UTC = RealDate.UTC;
46021
- // eslint-disable-next-line no-global-assign
46022
46736
  Date = FrozenDate;
46023
46737
  globalThis.Date = FrozenDate;
46024
46738
  if (typeof performance !== "undefined") {
@@ -46026,11 +46740,9 @@ function buildFreezeInitScript() {
46026
46740
  const realNow = performance.now.bind(performance);
46027
46741
  performance.now = () => realNow() - origin;
46028
46742
  }
46029
- // Deterministic Math.random (mulberry32, fixed seed). Reusable capture
46030
- // pages reset the seed before mounting each storyboard.
46031
46743
  let seed;
46032
46744
  globalThis.__tempoResetCaptureDeterminism = () => {
46033
- seed = 0x74656d70; // "temp"
46745
+ seed = 0x74656d70;
46034
46746
  };
46035
46747
  globalThis.__tempoResetCaptureDeterminism();
46036
46748
  Math.random = () => {
@@ -46053,6 +46765,14 @@ var FREEZE_MOTION_CSS = `
46053
46765
  caret-color: transparent !important;
46054
46766
  }
46055
46767
  `;
46768
+
46769
+ // ../canvas-capture-headless/src/determinism.ts
46770
+ var CHROMIUM_LAUNCH_ARGS = [
46771
+ "--force-color-profile=srgb",
46772
+ "--force-device-scale-factor=2",
46773
+ "--hide-scrollbars",
46774
+ "--disable-lcd-text"
46775
+ ];
46056
46776
  async function runSettleStep(name, work, onStep) {
46057
46777
  const startedAt = Date.now();
46058
46778
  try {
@@ -46143,19 +46863,19 @@ async function settlePage(page, targetSelector, options = {}) {
46143
46863
  // ../canvas-capture-headless/src/ensure-chromium.ts
46144
46864
  import { spawn as spawn7 } from "child_process";
46145
46865
  import { createRequire as createRequire5 } from "module";
46146
- import path55 from "path";
46866
+ import path56 from "path";
46147
46867
  var INSTALL_TIMEOUT_MS = 8 * 60 * 1e3;
46148
46868
  function isMissingBrowserError(err) {
46149
46869
  const message = err instanceof Error ? err.message : String(err);
46150
- return message.includes("Executable doesn't exist") || message.includes("playwright install");
46870
+ return message.includes("Executable doesn't exist") || /playwright install(?!-deps)/.test(message);
46151
46871
  }
46152
46872
  function autoInstallDisabled() {
46153
46873
  return process.env.TEMPO_MCP_NO_AUTO_INSTALL === "1";
46154
46874
  }
46155
46875
  async function installChromium(log, spawnImpl = spawn7) {
46156
46876
  const require3 = createRequire5(import.meta.url);
46157
- const cliPath = path55.join(
46158
- path55.dirname(require3.resolve("playwright/package.json")),
46877
+ const cliPath = path56.join(
46878
+ path56.dirname(require3.resolve("playwright/package.json")),
46159
46879
  "cli.js"
46160
46880
  );
46161
46881
  await new Promise((resolve2, reject) => {
@@ -46599,7 +47319,7 @@ function sha256Hex(bytes) {
46599
47319
  function looksBlank(pngBytes) {
46600
47320
  let png;
46601
47321
  try {
46602
- png = import_pngjs.PNG.sync.read(Buffer.from(pngBytes));
47322
+ png = import_pngjs2.PNG.sync.read(Buffer.from(pngBytes));
46603
47323
  } catch {
46604
47324
  return true;
46605
47325
  }
@@ -46612,6 +47332,86 @@ function looksBlank(pngBytes) {
46612
47332
  }
46613
47333
  return true;
46614
47334
  }
47335
+ function isNavigationInterruptedError(message) {
47336
+ return /Execution context was destroyed|most likely because of a navigation|Cannot find context with specified id|[Ff]rame was detached|Navigation interrupted|is interrupted by another navigation|Inspected target navigated or closed|Cannot find object with id|context was destroyed/.test(
47337
+ message
47338
+ );
47339
+ }
47340
+ function isPageGoneError(message) {
47341
+ return /Target closed|Target page, context or browser has been closed|Page closed|Target crashed/.test(
47342
+ message
47343
+ );
47344
+ }
47345
+ function isBrowserGoneError(message) {
47346
+ return /Browser has been closed|Browser closed|browser has disconnected|Browser process crashed/.test(
47347
+ message
47348
+ );
47349
+ }
47350
+ var navigatedErrors = /* @__PURE__ */ new WeakSet();
47351
+ function markNavigated(error, navigated) {
47352
+ if (navigated && error !== null && typeof error === "object") {
47353
+ navigatedErrors.add(error);
47354
+ }
47355
+ }
47356
+ function readNavigatedMark(error) {
47357
+ return error !== null && typeof error === "object" && navigatedErrors.has(error);
47358
+ }
47359
+ function documentReplaced(start, now, expectedNavigations) {
47360
+ const ownSameDocument = now.sameDocumentIssued - start.sameDocumentIssued;
47361
+ return now.navigationRequests > start.navigationRequests + expectedNavigations || now.frameNavigations > start.frameNavigations + expectedNavigations + ownSameDocument;
47362
+ }
47363
+ var BOARD_ATTEMPT_LIMIT = 3;
47364
+ function decideBoardRetry(attempt, attemptIndex, attemptLimit = BOARD_ATTEMPT_LIMIT) {
47365
+ if (attemptIndex >= attemptLimit - 1) return "accept";
47366
+ if (attempt.status === "render-error") {
47367
+ const message = attempt.error ?? "";
47368
+ if (isBrowserGoneError(message)) return "accept";
47369
+ if (isPageGoneError(message)) return "retry-fresh-worker";
47370
+ if (attempt.navigated || isNavigationInterruptedError(message)) {
47371
+ return "retry";
47372
+ }
47373
+ return "accept";
47374
+ }
47375
+ return attempt.navigated ? "retry" : "accept";
47376
+ }
47377
+ function navigatedRejectionError(attempts) {
47378
+ return `document reloaded during every attempt (${attempts}); the capture never saw one stable document`;
47379
+ }
47380
+ function finalizeAcceptedAttempt(attempt, attempts) {
47381
+ const { result } = attempt;
47382
+ if (attempt.navigated && result.status !== "render-error") {
47383
+ return {
47384
+ canvasPath: result.canvasPath,
47385
+ storyboardId: result.storyboardId,
47386
+ status: "render-error",
47387
+ error: navigatedRejectionError(attempts),
47388
+ timings: result.timings
47389
+ };
47390
+ }
47391
+ if (result.status === "render-error" && attempts > 1) {
47392
+ return { ...result, error: `after ${attempts} attempts: ${result.error}` };
47393
+ }
47394
+ return result;
47395
+ }
47396
+ function reloadStormWindowFor(parallelPages) {
47397
+ return Math.max(8, 4 * Math.max(1, Math.floor(parallelPages)));
47398
+ }
47399
+ function isReloadStorm(recent, window2) {
47400
+ if (recent.length < window2) return false;
47401
+ return recent.slice(-window2).every(Boolean);
47402
+ }
47403
+ var CaptureReloadStormError = class extends Error {
47404
+ constructor() {
47405
+ super(...arguments);
47406
+ this.name = "CaptureReloadStormError";
47407
+ }
47408
+ };
47409
+ function describeRetryReason(attempt) {
47410
+ if (attempt.status === "render-error" && isPageGoneError(attempt.error ?? "")) {
47411
+ return "page closed mid-capture";
47412
+ }
47413
+ return "document reloaded mid-capture";
47414
+ }
46615
47415
  var DEFAULT_VIEWPORT = { width: 1600, height: 1200 };
46616
47416
  async function startCaptureSession(options) {
46617
47417
  const log = options.log ?? (() => {
@@ -46633,6 +47433,7 @@ async function startCaptureSession(options) {
46633
47433
  os: process.platform,
46634
47434
  browserVersion: browser.version()
46635
47435
  });
47436
+ const browserGoneError = (cause) => new Error(`capture browser is gone; cannot open a fresh page: ${cause}`);
46636
47437
  const createCaptureWorker = async (capture) => {
46637
47438
  const contextStart = Date.now();
46638
47439
  const context = await browser.newContext({
@@ -46653,6 +47454,13 @@ async function startCaptureSession(options) {
46653
47454
  contextMs: Date.now() - contextStart,
46654
47455
  contextClaimed: false,
46655
47456
  hasNavigated: false,
47457
+ counters: {
47458
+ navigationRequests: 0,
47459
+ frameNavigations: 0,
47460
+ sameDocumentIssued: 0
47461
+ },
47462
+ documentsLoaded: 0,
47463
+ navigationsInFlight: 0,
46656
47464
  async dispose() {
46657
47465
  await page.close().catch(() => {
46658
47466
  });
@@ -46660,9 +47468,64 @@ async function startCaptureSession(options) {
46660
47468
  });
46661
47469
  }
46662
47470
  };
47471
+ const isMainFrameNavigation = (request) => {
47472
+ try {
47473
+ return request.isNavigationRequest() && request.frame() === page.mainFrame();
47474
+ } catch {
47475
+ return false;
47476
+ }
47477
+ };
47478
+ page.on("request", (request) => {
47479
+ if (!isMainFrameNavigation(request)) return;
47480
+ if (request.redirectedFrom()) return;
47481
+ worker.counters.navigationRequests += 1;
47482
+ worker.navigationsInFlight += 1;
47483
+ });
47484
+ page.on("requestfailed", (request) => {
47485
+ if (!isMainFrameNavigation(request)) return;
47486
+ worker.navigationsInFlight = Math.max(0, worker.navigationsInFlight - 1);
47487
+ });
47488
+ page.on("framenavigated", (frame) => {
47489
+ if (frame === page.mainFrame()) worker.counters.frameNavigations += 1;
47490
+ });
47491
+ page.on("domcontentloaded", () => {
47492
+ worker.documentsLoaded += 1;
47493
+ worker.navigationsInFlight = 0;
47494
+ if (worker.hasNavigated) {
47495
+ page.addStyleTag({ content: FREEZE_MOTION_CSS }).catch(() => {
47496
+ });
47497
+ }
47498
+ });
46663
47499
  options.timing?.recordBoard("context", worker.contextMs);
46664
47500
  return worker;
46665
47501
  };
47502
+ const sleep6 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
47503
+ const awaitDocumentQuiet = async (worker, timeoutMs) => {
47504
+ const quietMs = 500;
47505
+ const deadline = Date.now() + Math.min(timeoutMs, 1e4);
47506
+ const settleLoad = async () => {
47507
+ while (worker.navigationsInFlight > 0 && Date.now() < deadline) {
47508
+ await sleep6(50);
47509
+ }
47510
+ await worker.page.waitForLoadState("load", {
47511
+ timeout: Math.max(1, deadline - Date.now())
47512
+ }).catch(() => {
47513
+ });
47514
+ };
47515
+ await settleLoad();
47516
+ let requests = worker.counters.navigationRequests;
47517
+ let quietSince = Date.now();
47518
+ while (Date.now() < deadline) {
47519
+ await sleep6(100);
47520
+ if (worker.counters.navigationRequests !== requests || worker.navigationsInFlight > 0) {
47521
+ requests = worker.counters.navigationRequests;
47522
+ quietSince = Date.now();
47523
+ await settleLoad();
47524
+ } else if (Date.now() - quietSince >= quietMs) {
47525
+ return;
47526
+ }
47527
+ }
47528
+ };
46666
47529
  const withSettledStoryboard = async (target, capture, timings, work, reusableWorker) => {
46667
47530
  const renderId = storyboardRenderId(
46668
47531
  target.hostCanvasPath,
@@ -46676,6 +47539,9 @@ async function startCaptureSession(options) {
46676
47539
  timings.contextMs = worker.contextMs;
46677
47540
  worker.contextClaimed = true;
46678
47541
  }
47542
+ const countersAtStart = { ...worker.counters };
47543
+ const expectedNavigations = worker.hasNavigated ? 0 : 1;
47544
+ const navigatedSinceStart = () => documentReplaced(countersAtStart, worker.counters, expectedNavigations);
46679
47545
  try {
46680
47546
  const navStart = Date.now();
46681
47547
  try {
@@ -46692,6 +47558,7 @@ async function startCaptureSession(options) {
46692
47558
  history.replaceState(null, "", url);
46693
47559
  window.dispatchEvent(new PopStateEvent("popstate"));
46694
47560
  }, idleUrl.toString());
47561
+ worker.counters.sameDocumentIssued += 1;
46695
47562
  await page.waitForSelector(worker.currentSelector, {
46696
47563
  state: "detached",
46697
47564
  timeout: capture.settleTimeoutMs ?? options.settleTimeoutMs ?? 3e4
@@ -46708,6 +47575,7 @@ async function startCaptureSession(options) {
46708
47575
  },
46709
47576
  { url: targetUrl, nextSelector: selector }
46710
47577
  );
47578
+ worker.counters.sameDocumentIssued += 1;
46711
47579
  }
46712
47580
  worker.currentSelector = selector;
46713
47581
  } finally {
@@ -46798,12 +47666,16 @@ async function startCaptureSession(options) {
46798
47666
  profile.longTaskCount,
46799
47667
  "count"
46800
47668
  );
46801
- return await work(page, selector);
47669
+ const value = await work(page, selector);
47670
+ return { value, navigated: navigatedSinceStart() };
47671
+ } catch (error) {
47672
+ markNavigated(error, navigatedSinceStart());
47673
+ throw error;
46802
47674
  } finally {
46803
47675
  if (ownsWorker) await worker.dispose();
46804
47676
  }
46805
47677
  };
46806
- const captureStoryboardArtifacts = async (request) => {
47678
+ const captureStoryboardArtifactsOnce = async (request) => {
46807
47679
  const artifacts = request.artifacts ?? ["png"];
46808
47680
  const timings = { contextMs: 0, navigateMs: 0, settleMs: 0 };
46809
47681
  return withSettledStoryboard(
@@ -46817,7 +47689,7 @@ async function startCaptureSession(options) {
46817
47689
  const pngBytes = new Uint8Array(
46818
47690
  await page.locator(selector).screenshot({ type: "png", animations: "disabled" })
46819
47691
  );
46820
- const decoded = import_pngjs.PNG.sync.read(Buffer.from(pngBytes));
47692
+ const decoded = import_pngjs2.PNG.sync.read(Buffer.from(pngBytes));
46821
47693
  result.png = {
46822
47694
  pngBytes,
46823
47695
  width: decoded.width,
@@ -46857,6 +47729,31 @@ async function startCaptureSession(options) {
46857
47729
  }
46858
47730
  );
46859
47731
  };
47732
+ const captureStoryboardArtifacts = async (request) => {
47733
+ const key = `${request.hostCanvasPath}#${request.storyboardId}`;
47734
+ for (let attemptIndex = 0; ; attemptIndex += 1) {
47735
+ const lastAttempt = attemptIndex >= BOARD_ATTEMPT_LIMIT - 1;
47736
+ const attempts = attemptIndex + 1;
47737
+ let outcome;
47738
+ try {
47739
+ outcome = await captureStoryboardArtifactsOnce(request);
47740
+ } catch (error) {
47741
+ const message = error instanceof Error ? error.message : String(error);
47742
+ if (!browser.isConnected()) throw browserGoneError(message);
47743
+ const retryable = !isBrowserGoneError(message) && (readNavigatedMark(error) || isNavigationInterruptedError(message) || isPageGoneError(message));
47744
+ if (lastAttempt || !retryable) throw error;
47745
+ log(
47746
+ `[capture] ${key}: ${message.split("\n")[0]} (attempt ${attempts}/${BOARD_ATTEMPT_LIMIT}) \u2014 re-capturing`
47747
+ );
47748
+ continue;
47749
+ }
47750
+ if (!outcome.navigated) return outcome.value;
47751
+ if (lastAttempt) throw new Error(navigatedRejectionError(attempts));
47752
+ log(
47753
+ `[capture] ${key}: document reloaded mid-capture (attempt ${attempts}/${BOARD_ATTEMPT_LIMIT}) \u2014 re-capturing`
47754
+ );
47755
+ }
47756
+ };
46860
47757
  const captureOne = async (board, worker) => {
46861
47758
  const timings = {
46862
47759
  contextMs: 0,
@@ -46865,7 +47762,7 @@ async function startCaptureSession(options) {
46865
47762
  screenshotMs: 0
46866
47763
  };
46867
47764
  try {
46868
- return await withSettledStoryboard(
47765
+ const { value, navigated } = await withSettledStoryboard(
46869
47766
  {
46870
47767
  hostCanvasPath: board.hostCanvasPath,
46871
47768
  storyboardId: board.storyboardId,
@@ -46903,14 +47800,21 @@ async function startCaptureSession(options) {
46903
47800
  },
46904
47801
  worker
46905
47802
  );
47803
+ return { result: value, status: value.status, navigated };
46906
47804
  } catch (error) {
46907
- return {
47805
+ const result = {
46908
47806
  canvasPath: board.canvasPath,
46909
47807
  storyboardId: board.storyboardId,
46910
47808
  status: "render-error",
46911
47809
  error: error instanceof Error ? error.message : String(error),
46912
47810
  timings
46913
47811
  };
47812
+ return {
47813
+ result,
47814
+ status: result.status,
47815
+ error: result.error,
47816
+ navigated: readNavigatedMark(error)
47817
+ };
46914
47818
  }
46915
47819
  };
46916
47820
  return {
@@ -46919,33 +47823,90 @@ async function startCaptureSession(options) {
46919
47823
  captureBoards: async (boards, captureOptions) => {
46920
47824
  if (boards.length === 0) return [];
46921
47825
  const label = captureOptions?.label ?? "capture";
47826
+ const settleTimeoutMs = options.settleTimeoutMs ?? 3e4;
46922
47827
  const results = new Array(boards.length);
47828
+ const workerCount = Math.max(1, Math.min(parallelPages, boards.length));
47829
+ const stormWindow = reloadStormWindowFor(workerCount);
47830
+ const recentReloadRetries = [];
47831
+ let reloadRetriesTotal = 0;
47832
+ let aborted = false;
46923
47833
  let nextIndex = 0;
46924
47834
  let completed = 0;
46925
- const workers = Array.from(
46926
- { length: Math.max(1, Math.min(parallelPages, boards.length)) },
46927
- async () => {
46928
- let worker = await createCaptureWorker({});
47835
+ const workers = Array.from({ length: workerCount }, async () => {
47836
+ let worker = await createCaptureWorker({});
47837
+ const replaceWorker = async () => {
47838
+ await worker.dispose();
47839
+ if (!browser.isConnected()) {
47840
+ throw browserGoneError("browser disconnected");
47841
+ }
46929
47842
  try {
46930
- for (; ; ) {
46931
- const index = nextIndex;
46932
- nextIndex += 1;
46933
- if (index >= boards.length) return;
46934
- results[index] = await captureOne(boards[index], worker);
46935
- completed += 1;
46936
- if (completed % 10 === 0 || completed === boards.length) {
46937
- log(`${label}: ${completed}/${boards.length}`);
47843
+ worker = await createCaptureWorker({});
47844
+ } catch (error) {
47845
+ throw browserGoneError(
47846
+ error instanceof Error ? error.message : String(error)
47847
+ );
47848
+ }
47849
+ };
47850
+ try {
47851
+ for (; ; ) {
47852
+ if (aborted) return;
47853
+ const index = nextIndex;
47854
+ nextIndex += 1;
47855
+ if (index >= boards.length) return;
47856
+ const board = boards[index];
47857
+ const key = `${board.canvasPath}#${board.storyboardId}`;
47858
+ let result;
47859
+ let reloadRetries = 0;
47860
+ for (let attemptIndex = 0; ; attemptIndex += 1) {
47861
+ const attempt = await captureOne(board, worker);
47862
+ const decision = decideBoardRetry(attempt, attemptIndex);
47863
+ if (decision === "accept") {
47864
+ result = finalizeAcceptedAttempt(attempt, attemptIndex + 1);
47865
+ if (attemptIndex > 0) {
47866
+ options.timing?.recordBoard(
47867
+ "capture.retries",
47868
+ attemptIndex,
47869
+ "count"
47870
+ );
47871
+ }
47872
+ break;
46938
47873
  }
46939
- if (results[index].status === "render-error") {
46940
- await worker.dispose();
46941
- worker = await createCaptureWorker({});
47874
+ const reason = describeRetryReason(attempt);
47875
+ if (reason === "document reloaded mid-capture")
47876
+ reloadRetries += 1;
47877
+ log(
47878
+ `${label}: ${key}: ${reason} (attempt ${attemptIndex + 1}/${BOARD_ATTEMPT_LIMIT}) \u2014 re-capturing`
47879
+ );
47880
+ if (decision === "retry-fresh-worker") {
47881
+ await replaceWorker();
47882
+ } else {
47883
+ await awaitDocumentQuiet(worker, settleTimeoutMs);
46942
47884
  }
46943
47885
  }
46944
- } finally {
46945
- await worker.dispose();
47886
+ results[index] = result;
47887
+ completed += 1;
47888
+ if (completed % 10 === 0 || completed === boards.length) {
47889
+ log(`${label}: ${completed}/${boards.length}`);
47890
+ }
47891
+ reloadRetriesTotal += reloadRetries;
47892
+ recentReloadRetries.push(reloadRetries > 0);
47893
+ if (recentReloadRetries.length > stormWindow) {
47894
+ recentReloadRetries.shift();
47895
+ }
47896
+ if (isReloadStorm(recentReloadRetries, stormWindow)) {
47897
+ aborted = true;
47898
+ throw new CaptureReloadStormError(
47899
+ `capture aborted: the devserver reloaded during the last ${stormWindow} storyboards in a row (${reloadRetriesTotal} re-captures so far) \u2014 it is re-optimizing or restarting continuously; see the "devserver:" lines above`
47900
+ );
47901
+ }
47902
+ if (result.status === "render-error") {
47903
+ await replaceWorker();
47904
+ }
46946
47905
  }
47906
+ } finally {
47907
+ await worker.dispose();
46947
47908
  }
46948
- );
47909
+ });
46949
47910
  await Promise.all(workers);
46950
47911
  return results;
46951
47912
  },
@@ -46973,7 +47934,7 @@ async function startCaptureSession(options) {
46973
47934
  }
46974
47935
 
46975
47936
  // ../canvas-capture-headless/src/composite.ts
46976
- var import_pngjs2 = __toESM(require_png(), 1);
47937
+ var import_pngjs3 = __toESM(require_png(), 1);
46977
47938
  var DEFAULT_PADDING = 24;
46978
47939
  function normalizeCompositePadding(value, contentWidth, contentHeight) {
46979
47940
  if (value == null) return DEFAULT_PADDING;
@@ -47003,7 +47964,7 @@ function compositeStoryboardPngs(images, options = {}) {
47003
47964
  throw new Error("Composite image inputs must have finite positions");
47004
47965
  }
47005
47966
  return {
47006
- png: import_pngjs2.PNG.sync.read(Buffer.from(image.pngBytes)),
47967
+ png: import_pngjs3.PNG.sync.read(Buffer.from(image.pngBytes)),
47007
47968
  x: Math.round(image.x * scale),
47008
47969
  y: Math.round(image.y * scale)
47009
47970
  };
@@ -47027,7 +47988,7 @@ function compositeStoryboardPngs(images, options = {}) {
47027
47988
  );
47028
47989
  const width = contentWidth + padding * 2;
47029
47990
  const height = contentHeight + padding * 2;
47030
- const output = new import_pngjs2.PNG({ width, height });
47991
+ const output = new import_pngjs3.PNG({ width, height });
47031
47992
  if (options.background) {
47032
47993
  const { r, g, b: b2 } = options.background;
47033
47994
  const a = Math.round((options.background.a ?? 1) * 255);
@@ -47066,17 +48027,17 @@ function compositeStoryboardPngs(images, options = {}) {
47066
48027
  }
47067
48028
  }
47068
48029
  return {
47069
- pngBytes: new Uint8Array(import_pngjs2.PNG.sync.write(output)),
48030
+ pngBytes: new Uint8Array(import_pngjs3.PNG.sync.write(output)),
47070
48031
  width,
47071
48032
  height
47072
48033
  };
47073
48034
  }
47074
48035
 
47075
48036
  // ../canvas-capture-headless/src/devserver.ts
47076
- import { readFile as readFile24 } from "fs/promises";
47077
- import path56 from "path";
48037
+ import { readFile as readFile25 } from "fs/promises";
48038
+ import path57 from "path";
47078
48039
  async function readTempoDevserverConfig(tempoConfigPath) {
47079
- const config = JSON.parse(await readFile24(tempoConfigPath, "utf-8"));
48040
+ const config = JSON.parse(await readFile25(tempoConfigPath, "utf-8"));
47080
48041
  if (!config.scripts?.install || !config.scripts?.start) {
47081
48042
  throw new Error(
47082
48043
  `tempo.config.json at ${tempoConfigPath} must declare scripts.install and scripts.start`
@@ -47092,9 +48053,9 @@ async function readTempoDevserverConfig(tempoConfigPath) {
47092
48053
  };
47093
48054
  }
47094
48055
  function resolveTempoDir(options) {
47095
- if (options.tempoConfigPath) return path56.dirname(options.tempoConfigPath);
48056
+ if (options.tempoConfigPath) return path57.dirname(options.tempoConfigPath);
47096
48057
  if (options.tempoDir) return options.tempoDir;
47097
- if (options.repoRoot) return path56.join(options.repoRoot, "tempo");
48058
+ if (options.repoRoot) return path57.join(options.repoRoot, "tempo");
47098
48059
  throw new Error(
47099
48060
  "startDevserverSession needs one of tempoDir, repoRoot, or tempoConfigPath"
47100
48061
  );
@@ -47130,11 +48091,30 @@ async function awaitCanvasRegistration(options) {
47130
48091
  await new Promise((resolve2) => setTimeout(resolve2, retryDelayMs));
47131
48092
  }
47132
48093
  }
48094
+ var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
48095
+ function cleanDevserverLine(line) {
48096
+ return line.replace(ANSI_RE, "").trim();
48097
+ }
48098
+ function createDevserverLineSplitter() {
48099
+ let remainder = "";
48100
+ return {
48101
+ push(chunk) {
48102
+ const parts = (remainder + chunk).split(/\r?\n/);
48103
+ remainder = parts.pop() ?? "";
48104
+ return parts.map(cleanDevserverLine).filter((line) => line.length > 0);
48105
+ },
48106
+ flush() {
48107
+ const tail = cleanDevserverLine(remainder);
48108
+ remainder = "";
48109
+ return tail.length > 0 ? [tail] : [];
48110
+ }
48111
+ };
48112
+ }
47133
48113
  async function startDevserverSession(options) {
47134
48114
  const log = options.log ?? (() => {
47135
48115
  });
47136
48116
  const tempoDir = resolveTempoDir(options);
47137
- const tempoConfigPath = options.tempoConfigPath ?? path56.join(tempoDir, "tempo.config.json");
48117
+ const tempoConfigPath = options.tempoConfigPath ?? path57.join(tempoDir, "tempo.config.json");
47138
48118
  const config = await readTempoDevserverConfig(tempoConfigPath);
47139
48119
  const hostCanvasPaths = options.hostCanvasPaths ?? [];
47140
48120
  const devServer = new TempoDevServer({
@@ -47143,18 +48123,36 @@ async function startDevserverSession(options) {
47143
48123
  scripts: config.scripts
47144
48124
  });
47145
48125
  await devServer.initialize();
47146
- const projectRoot = path56.dirname(tempoDir);
48126
+ const projectRoot = path57.dirname(tempoDir);
47147
48127
  for (const hostCanvasPath of hostCanvasPaths) {
47148
48128
  await devServer.ensureCanvasRegistered(
47149
- path56.join(projectRoot, hostCanvasPath)
48129
+ path57.join(projectRoot, hostCanvasPath)
47150
48130
  );
47151
48131
  }
47152
48132
  const handle = await devServer.startDevServer();
48133
+ const onOutput = options.onOutput;
48134
+ const outputLines = onOutput ? createDevserverLineSplitter() : null;
48135
+ const forwardOutput = (lines) => {
48136
+ if (!onOutput) return;
48137
+ for (const line of lines) {
48138
+ try {
48139
+ onOutput(line);
48140
+ } catch (error) {
48141
+ log(
48142
+ `devserver output listener threw: ${error instanceof Error ? error.message : String(error)}`
48143
+ );
48144
+ }
48145
+ }
48146
+ };
48147
+ if (outputLines) {
48148
+ handle.onOutput((data) => forwardOutput(outputLines.push(String(data))));
48149
+ }
47153
48150
  const ready = await handle.waitUntilReady(options.readyTimeoutMs ?? 12e4);
47154
48151
  if (!ready.ok || !ready.url) {
47155
48152
  await handle.stop().catch(() => {
47156
48153
  });
47157
48154
  await devServer.dispose();
48155
+ forwardOutput(outputLines?.flush() ?? []);
47158
48156
  throw new Error(
47159
48157
  `tempobook devserver failed to become ready: ${ready.message ?? (ready.timedOut ? "timeout" : "unknown")}`
47160
48158
  );
@@ -47164,12 +48162,14 @@ async function startDevserverSession(options) {
47164
48162
  log(`devserver ready at ${hostUrl}`);
47165
48163
  const registerCanvas = async (hostCanvasPath) => {
47166
48164
  await devServer.ensureCanvasRegistered(
47167
- path56.join(projectRoot, hostCanvasPath)
48165
+ path57.join(projectRoot, hostCanvasPath)
47168
48166
  );
47169
48167
  await awaitCanvasRegistration({
47170
48168
  origin,
47171
48169
  hostCanvasPath,
47172
- reRegister: () => devServer.ensureCanvasRegistered(path56.join(projectRoot, hostCanvasPath)),
48170
+ reRegister: () => devServer.ensureCanvasRegistered(
48171
+ path57.join(projectRoot, hostCanvasPath)
48172
+ ),
47173
48173
  log
47174
48174
  });
47175
48175
  };
@@ -47177,7 +48177,9 @@ async function startDevserverSession(options) {
47177
48177
  await awaitCanvasRegistration({
47178
48178
  origin,
47179
48179
  hostCanvasPath,
47180
- reRegister: () => devServer.ensureCanvasRegistered(path56.join(projectRoot, hostCanvasPath)),
48180
+ reRegister: () => devServer.ensureCanvasRegistered(
48181
+ path57.join(projectRoot, hostCanvasPath)
48182
+ ),
47181
48183
  log
47182
48184
  });
47183
48185
  }
@@ -47191,26 +48193,27 @@ async function startDevserverSession(options) {
47191
48193
  await handle.stop().catch(() => {
47192
48194
  });
47193
48195
  await devServer.dispose();
48196
+ forwardOutput(outputLines?.flush() ?? []);
47194
48197
  }
47195
48198
  };
47196
48199
  }
47197
48200
 
47198
48201
  // ../canvas-capture-headless/src/enumerate.ts
47199
- import { readFile as readFile25 } from "fs/promises";
47200
- import path57 from "path";
48202
+ import { readFile as readFile26 } from "fs/promises";
48203
+ import path58 from "path";
47201
48204
  function toPosixPath7(value) {
47202
- return value.split(path57.sep).join("/");
48205
+ return value.split(path58.sep).join("/");
47203
48206
  }
47204
48207
  async function enumerateCanvasStoryboards(options) {
47205
- const pagesDir = options.pagesDir ?? path57.join(options.tempoDir, "designs");
47206
- const projectRoot = path57.dirname(options.tempoDir);
48208
+ const pagesDir = options.pagesDir ?? path58.join(options.tempoDir, "designs");
48209
+ const projectRoot = path58.dirname(options.tempoDir);
47207
48210
  const canvasFiles = await scanCanvasFiles(pagesDir);
47208
48211
  const storyboards = [];
47209
48212
  const structuralErrors = [];
47210
48213
  const vrtConfigErrors = [];
47211
48214
  let totalStoryboards = 0;
47212
48215
  for (const filePath of canvasFiles) {
47213
- const source = await readFile25(filePath, "utf-8");
48216
+ const source = await readFile26(filePath, "utf-8");
47214
48217
  const parsed = parseCanvasFile(filePath, source);
47215
48218
  const structural = parsed.errors.filter(
47216
48219
  (error) => error.kind === "structural"
@@ -47238,9 +48241,9 @@ async function enumerateCanvasStoryboards(options) {
47238
48241
  continue;
47239
48242
  }
47240
48243
  const canvasPath = toPosixPath7(
47241
- path57.relative(options.tempoDir, path57.dirname(filePath))
48244
+ path58.relative(options.tempoDir, path58.dirname(filePath))
47242
48245
  );
47243
- const hostCanvasPath = toPosixPath7(path57.relative(projectRoot, filePath));
48246
+ const hostCanvasPath = toPosixPath7(path58.relative(projectRoot, filePath));
47244
48247
  for (const storyboard of parsed.storyboards) {
47245
48248
  totalStoryboards += 1;
47246
48249
  if (storyboard.kind !== "component") continue;
@@ -47288,7 +48291,7 @@ function describeStyleStarvation(assessment) {
47288
48291
  import { createHash as createHash5 } from "crypto";
47289
48292
  import fs6 from "fs/promises";
47290
48293
  import os3 from "os";
47291
- import path58 from "path";
48294
+ import path59 from "path";
47292
48295
 
47293
48296
  // ../workspaces/git-worktree/non-interactive-git.ts
47294
48297
  var NON_INTERACTIVE_GIT_CONFIG = [
@@ -47409,7 +48412,7 @@ async function checkBranchOnRemote(repoPath, branch) {
47409
48412
 
47410
48413
  // ../workspaces/git-worktree/canvas-scan-cache.ts
47411
48414
  import fs7 from "fs/promises";
47412
- import path59 from "path";
48415
+ import path60 from "path";
47413
48416
  var DEFAULT_CONCURRENCY = 6;
47414
48417
  var DEFAULT_MAX_AGE_MS = 5 * 6e4;
47415
48418
  var Semaphore = class {
@@ -47435,15 +48438,15 @@ var sharedSemaphore = new Semaphore(DEFAULT_CONCURRENCY);
47435
48438
 
47436
48439
  // src/canvas-hooks.ts
47437
48440
  var MAX_MCP_SCREENSHOT_DIMENSION_PX = 1990;
47438
- function toPosix(value) {
47439
- return value.split(path60.sep).join("/");
48441
+ function toPosix2(value) {
48442
+ return value.split(path61.sep).join("/");
47440
48443
  }
47441
48444
  function normalizeSlug(input) {
47442
48445
  return input.trim().replace(/\\/g, "/").replace(/\/+$/, "").replace(/\/[^/]*\.canvas\.tsx$/i, "");
47443
48446
  }
47444
48447
  async function isCanvasFolder(dir) {
47445
48448
  try {
47446
- return (await stat10(path60.join(dir, "index.canvas.tsx"))).isFile();
48449
+ return (await stat10(path61.join(dir, "index.canvas.tsx"))).isFile();
47447
48450
  } catch {
47448
48451
  return false;
47449
48452
  }
@@ -47451,18 +48454,18 @@ async function isCanvasFolder(dir) {
47451
48454
  async function resolveCanvas(workspace, canvasSlug) {
47452
48455
  const workspaceRoot = workspace.workspaceRoot;
47453
48456
  const canvasesDir = workspace.canvasesDir;
47454
- const tempoDir = path60.join(workspaceRoot, "tempo");
48457
+ const tempoDir = path61.join(workspaceRoot, "tempo");
47455
48458
  const enumeration = await enumerateCanvasStoryboards({
47456
48459
  tempoDir,
47457
48460
  pagesDir: canvasesDir
47458
48461
  });
47459
48462
  const byFolder = /* @__PURE__ */ new Map();
47460
48463
  for (const storyboard of enumeration.storyboards) {
47461
- const folder = path60.dirname(storyboard.filePath);
48464
+ const folder = path61.dirname(storyboard.filePath);
47462
48465
  let canvas = byFolder.get(folder);
47463
48466
  if (!canvas) {
47464
48467
  canvas = {
47465
- contentPath: toPosix(path60.relative(workspaceRoot, folder)),
48468
+ contentPath: toPosix2(path61.relative(workspaceRoot, folder)),
47466
48469
  hostCanvasPath: storyboard.hostCanvasPath,
47467
48470
  storyboards: []
47468
48471
  };
@@ -47472,15 +48475,15 @@ async function resolveCanvas(workspace, canvasSlug) {
47472
48475
  }
47473
48476
  const slug = normalizeSlug(canvasSlug);
47474
48477
  if (!slug) return { kind: "not_found" };
47475
- if (slug.includes("/") || path60.isAbsolute(canvasSlug)) {
48478
+ if (slug.includes("/") || path61.isAbsolute(canvasSlug)) {
47476
48479
  for (const [folder, canvas] of byFolder) {
47477
48480
  const candidates = [
47478
- toPosix(path60.relative(canvasesDir, folder)),
47479
- toPosix(path60.relative(workspaceRoot, folder)),
47480
- toPosix(path60.relative(tempoDir, folder))
48481
+ toPosix2(path61.relative(canvasesDir, folder)),
48482
+ toPosix2(path61.relative(workspaceRoot, folder)),
48483
+ toPosix2(path61.relative(tempoDir, folder))
47481
48484
  ];
47482
- if (path60.isAbsolute(canvasSlug)) {
47483
- if (path60.resolve(canvasSlug) === folder) {
48485
+ if (path61.isAbsolute(canvasSlug)) {
48486
+ if (path61.resolve(canvasSlug) === folder) {
47484
48487
  return { kind: "ok", canvas };
47485
48488
  }
47486
48489
  continue;
@@ -47489,29 +48492,29 @@ async function resolveCanvas(workspace, canvasSlug) {
47489
48492
  }
47490
48493
  } else {
47491
48494
  const matches = [...byFolder.entries()].filter(
47492
- ([folder]) => path60.basename(folder) === slug
48495
+ ([folder]) => path61.basename(folder) === slug
47493
48496
  );
47494
48497
  if (matches.length > 1) {
47495
48498
  return {
47496
48499
  kind: "ambiguous",
47497
- candidates: matches.map(([folder]) => toPosix(path60.relative(canvasesDir, folder))).sort()
48500
+ candidates: matches.map(([folder]) => toPosix2(path61.relative(canvasesDir, folder))).sort()
47498
48501
  };
47499
48502
  }
47500
48503
  if (matches.length === 1) return { kind: "ok", canvas: matches[0][1] };
47501
48504
  }
47502
- const probeDirs = path60.isAbsolute(canvasSlug) ? [path60.resolve(canvasSlug)] : [
47503
- path60.join(canvasesDir, slug),
47504
- path60.join(workspaceRoot, slug),
47505
- path60.join(tempoDir, slug)
48505
+ const probeDirs = path61.isAbsolute(canvasSlug) ? [path61.resolve(canvasSlug)] : [
48506
+ path61.join(canvasesDir, slug),
48507
+ path61.join(workspaceRoot, slug),
48508
+ path61.join(tempoDir, slug)
47506
48509
  ];
47507
48510
  for (const dir of probeDirs) {
47508
48511
  if (await isCanvasFolder(dir)) {
47509
48512
  return {
47510
48513
  kind: "ok",
47511
48514
  canvas: {
47512
- contentPath: toPosix(path60.relative(workspaceRoot, dir)),
47513
- hostCanvasPath: toPosix(
47514
- path60.relative(workspaceRoot, path60.join(dir, "index.canvas.tsx"))
48515
+ contentPath: toPosix2(path61.relative(workspaceRoot, dir)),
48516
+ hostCanvasPath: toPosix2(
48517
+ path61.relative(workspaceRoot, path61.join(dir, "index.canvas.tsx"))
47515
48518
  ),
47516
48519
  storyboards: []
47517
48520
  }
@@ -47554,7 +48557,7 @@ async function withCaptureSession(workspaceRoot, hostCanvasPath, settleTimeoutMs
47554
48557
  }
47555
48558
  }
47556
48559
  function downscalePngToFit(pngBytes, maxDim) {
47557
- const png = import_pngjs3.PNG.sync.read(Buffer.from(pngBytes));
48560
+ const png = import_pngjs4.PNG.sync.read(Buffer.from(pngBytes));
47558
48561
  if (png.width <= maxDim && png.height <= maxDim) {
47559
48562
  return {
47560
48563
  pngBase64: Buffer.from(pngBytes).toString("base64"),
@@ -47568,7 +48571,7 @@ function downscalePngToFit(pngBytes, maxDim) {
47568
48571
  maxDim,
47569
48572
  Math.max(1, Math.round(png.height * ratio))
47570
48573
  );
47571
- const out = new import_pngjs3.PNG({ width: outWidth, height: outHeight });
48574
+ const out = new import_pngjs4.PNG({ width: outWidth, height: outHeight });
47572
48575
  for (let y = 0; y < outHeight; y += 1) {
47573
48576
  const sy0 = Math.floor(y * png.height / outHeight);
47574
48577
  const sy1 = Math.max(
@@ -47602,7 +48605,7 @@ function downscalePngToFit(pngBytes, maxDim) {
47602
48605
  out.data[o + 3] = Math.round(a / count);
47603
48606
  }
47604
48607
  }
47605
- const bytes = import_pngjs3.PNG.sync.write(out);
48608
+ const bytes = import_pngjs4.PNG.sync.write(out);
47606
48609
  return {
47607
48610
  pngBase64: Buffer.from(bytes).toString("base64"),
47608
48611
  width: outWidth,
@@ -47794,6 +48797,80 @@ function createHeadlessCanvasHooks(options) {
47794
48797
  };
47795
48798
  }
47796
48799
  };
48800
+ const captureStoryboardStability = async (args, captureOptions) => {
48801
+ const resolution = await resolveCanvas(workspace, args.canvasSlug);
48802
+ if (resolution.kind === "ambiguous") {
48803
+ return {
48804
+ kind: "error",
48805
+ code: "ambiguous_canvas_slug",
48806
+ message: ambiguousCanvasSlugMessage(
48807
+ args.canvasSlug,
48808
+ resolution.candidates
48809
+ )
48810
+ };
48811
+ }
48812
+ if (resolution.kind === "not_found") {
48813
+ return {
48814
+ kind: "error",
48815
+ code: "canvas_not_open",
48816
+ message: `No canvas matching "${args.canvasSlug}" exists under ${workspace.canvasesDir}. Pass the canvas's folder name, or its path under the canvases root.`
48817
+ };
48818
+ }
48819
+ const canvas = resolution.canvas;
48820
+ const target = canvas.storyboards.find(
48821
+ (storyboard) => storyboard.storyboardId === args.storyboard
48822
+ );
48823
+ if (!target) {
48824
+ return {
48825
+ kind: "error",
48826
+ code: "storyboard_not_found",
48827
+ message: `Storyboard "${args.storyboard}" is not an export of canvas "${canvas.contentPath}".`,
48828
+ availableStoryboards: canvas.storyboards.map(
48829
+ (storyboard) => storyboard.storyboardId
48830
+ )
48831
+ };
48832
+ }
48833
+ try {
48834
+ const capturedRuns = await withCaptureSession(
48835
+ workspace.workspaceRoot,
48836
+ canvas.hostCanvasPath,
48837
+ captureOptions?.timeoutMs,
48838
+ async (session) => {
48839
+ let streamedRuns = 0;
48840
+ for (let run = 0; run < args.runs; run += 1) {
48841
+ if (captureOptions?.signal?.aborted) {
48842
+ throw captureOptions.signal.reason ?? new Error("Capture aborted");
48843
+ }
48844
+ const png = await session.captureStoryboard({
48845
+ hostCanvasPath: canvas.hostCanvasPath,
48846
+ storyboardId: target.storyboardId,
48847
+ width: target.layout.width,
48848
+ height: target.layout.height
48849
+ });
48850
+ if (looksBlank(png.pngBytes)) {
48851
+ throw new Error(
48852
+ `Storyboard "${target.storyboardId}" rendered an empty frame on run ${run + 1}/${args.runs}.`
48853
+ );
48854
+ }
48855
+ await captureOptions?.onCapture?.({
48856
+ pngBase64: Buffer.from(png.pngBytes).toString("base64"),
48857
+ width: png.width,
48858
+ height: png.height
48859
+ });
48860
+ streamedRuns += 1;
48861
+ }
48862
+ return streamedRuns;
48863
+ }
48864
+ );
48865
+ return { kind: "ok", runs: capturedRuns };
48866
+ } catch (error) {
48867
+ return {
48868
+ kind: "error",
48869
+ code: "capture_threw",
48870
+ message: isMissingPlaywright(error) ? PLAYWRIGHT_INSTALL_HINT : `Headless VRT stability capture failed: ${error instanceof Error ? error.message : String(error)}`
48871
+ };
48872
+ }
48873
+ };
47797
48874
  const createShareLink = async (args) => {
47798
48875
  const orgProjectId = options.getOrgProjectId();
47799
48876
  if (!orgProjectId) {
@@ -48012,6 +49089,10 @@ function createHeadlessCanvasHooks(options) {
48012
49089
  // canvas_screenshot tool must not try to lease one first.
48013
49090
  managesDevserver: true
48014
49091
  },
49092
+ canvasStoryboardStability: {
49093
+ captureRuns: captureStoryboardStability,
49094
+ managesDevserver: true
49095
+ },
48015
49096
  canvasShare: { create: createShareLink }
48016
49097
  };
48017
49098
  }
@@ -48133,4 +49214,4 @@ export {
48133
49214
  runServe,
48134
49215
  scopeParamsFor
48135
49216
  };
48136
- //# sourceMappingURL=serve-E62XRLQQ.js.map
49217
+ //# sourceMappingURL=serve-7RSR6WI6.js.map