waygraph 0.12.39 → 0.12.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Primary verbs (less is more):
6
6
  * auto Explore picker; `.flow.ts` / `--blocks From To` = run that path
7
- * demo Watch (step overlay); `--blocks` `--data` `--auto-next` `--fast` `--full` `--ff-expand` `--ff-disabled`
7
+ * demo Watch (step overlay); `--blocks` `--data` `--auto-next` `--fast` `--full` `--mini` `--ff-expand` `--ff-disabled`
8
8
  * run Execute; `--blocks` `--data` `--non-headless` `--video`
9
9
  *
10
10
  * Also: list / nav / validate / check / graph / init / agent-dive / traverse / try
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Primary verbs (less is more):
6
6
  * auto Explore picker; `.flow.ts` / `--blocks From To` = run that path
7
- * demo Watch (step overlay); `--blocks` `--data` `--auto-next` `--fast` `--full` `--ff-expand` `--ff-disabled`
7
+ * demo Watch (step overlay); `--blocks` `--data` `--auto-next` `--fast` `--full` `--mini` `--ff-expand` `--ff-disabled`
8
8
  * run Execute; `--blocks` `--data` `--non-headless` `--video`
9
9
  *
10
10
  * Also: list / nav / validate / check / graph / init / agent-dive / traverse / try
@@ -808,59 +808,11 @@ async function installOverlay(page, title) {
808
808
  const focus = !!opts.focus;
809
809
  const color = opts.color || "";
810
810
  const zoom = opts.zoom != null && opts.zoom > 1 ? opts.zoom : 1;
811
- const followMouse = opts.followMouse !== false && zoom > 1;
812
811
  const camMs = opts.camMs != null ? opts.camMs : 700;
813
- let settled = zoom <= 1;
814
- const settleAt = Date.now() + camMs;
815
812
  const style = { size, weight, focus, color };
816
- // Screen Studio / Screenify: dead zone follow (see followTick).
817
- const deadFrac = 0.5;
818
- let lastPanAt = 0;
819
- const readCursorViewport = () => {
820
- const cursor = document.getElementById("wg-cursor");
821
- if (cursor && cursor.style.opacity !== "0") {
822
- const r = cursor.getBoundingClientRect();
823
- if (r.width || r.height || r.left || r.top) {
824
- return { x: r.left, y: r.top };
825
- }
826
- }
827
- if (window.__wgCursorPos) return window.__wgCursorPos;
828
- return null;
829
- };
830
- const followTick = () => {
831
- if (!followMouse || !settled || window.__wgCameraLock) return;
832
- if (!window.__wgSetCamera || !window.__wgViewportToDoc) return;
833
- const cam = window.__wgCamera;
834
- if (!cam || !(cam.scale > 1.001)) return;
835
- // Skip while the demo cursor is mid CSS travel - reading a
836
- // mid-lerp rect makes the camera chase noise and teleport.
837
- if (window.__wgCursorTravelUntil && Date.now() < window.__wgCursorTravelUntil) {
838
- return;
839
- }
840
- const pos = readCursorViewport();
841
- if (!pos) return;
842
- const vw = window.innerWidth;
843
- const vh = window.innerHeight;
844
- const dx = pos.x - vw / 2;
845
- const dy = pos.y - vh / 2;
846
- const deadX = (vw * deadFrac) / 2;
847
- const deadY = (vh * deadFrac) / 2;
848
- let ox = 0;
849
- let oy = 0;
850
- if (dx > deadX) ox = dx - deadX;
851
- else if (dx < -deadX) ox = dx + deadX;
852
- if (dy > deadY) oy = dy - deadY;
853
- else if (dy < -deadY) oy = dy + deadY;
854
- if (ox === 0 && oy === 0) return;
855
- const now = Date.now();
856
- if (now - lastPanAt < 120) return;
857
- lastPanAt = now;
858
- // Partial gain - never yank the full overflow in one pan.
859
- const gain = 0.35;
860
- const nx = cam.cx + (ox / cam.scale) * gain;
861
- const ny = cam.cy + (oy / cam.scale) * gain;
862
- window.__wgSetCamera(nx, ny, cam.scale, 280);
863
- };
813
+ // Auto-center only. Never chase the host mouse (that fights the
814
+ // camera). Playwright's real mouse is driven separately by the CLI.
815
+ window.__wgIgnoreHostMouse = true;
864
816
  const tick = () => {
865
817
  const el = document.querySelector(sel);
866
818
  if (el && window.__wgPositionRing) {
@@ -872,8 +824,6 @@ async function installOverlay(page, title) {
872
824
  style,
873
825
  );
874
826
  }
875
- if (!settled && Date.now() >= settleAt) settled = true;
876
- followTick();
877
827
  window.__wgRingRaf = requestAnimationFrame(tick);
878
828
  };
879
829
  if (zoom > 1 && window.__wgSetCamera) {
@@ -882,41 +832,25 @@ async function installOverlay(page, title) {
882
832
  const already =
883
833
  window.__wgCamera && window.__wgCamera.scale > 1.001;
884
834
  const dur = already ? Math.min(camMs, 420) : camMs;
885
- // Focus must be document-space, not raw getBoundingClientRect
886
- // (viewport) - otherwise already-zoomed pans fight the cursor.
887
835
  const pt = window.__wgElDocPoint
888
836
  ? window.__wgElDocPoint(el0)
889
837
  : {
890
838
  x: el0.getBoundingClientRect().left + el0.getBoundingClientRect().width / 2,
891
839
  y: el0.getBoundingClientRect().top + el0.getBoundingClientRect().height / 2,
892
840
  };
841
+ // Lock for the whole ring - host mouse cannot steer.
893
842
  window.__wgCameraLock = true;
894
843
  window.__wgSetCamera(pt.x, pt.y, zoom, dur);
895
- // Unlock for dead-zone follow only when explicitly allowed.
896
- // zoomOut:false / cart-edge targets stay locked through dwell+click.
897
- if (followMouse && opts.allowFollow) {
898
- setTimeout(() => {
899
- window.__wgCameraLock = false;
900
- }, dur + 40);
901
- }
902
844
  }
903
845
  }
904
846
  window.__wgRingRaf = requestAnimationFrame(tick);
905
- // Real mouse only seeds cursor pos; dead-zone follow runs in RAF.
906
- const onMove = (ev) => {
907
- window.__wgCursorPos = { x: ev.clientX, y: ev.clientY };
908
- };
909
- window.addEventListener("mousemove", onMove, { passive: true });
910
- window.__wgRingTrack = onMove;
847
+ // Do NOT listen to host mousemove - it was steering the camera.
848
+ window.__wgRingTrack = null;
911
849
  window.__wgStopHighlightTrack = () => {
912
850
  if (window.__wgRingRaf) {
913
851
  cancelAnimationFrame(window.__wgRingRaf);
914
852
  window.__wgRingRaf = null;
915
853
  }
916
- if (window.__wgRingTrack) {
917
- window.removeEventListener("mousemove", window.__wgRingTrack);
918
- window.__wgRingTrack = null;
919
- }
920
854
  window.__wgStopHighlightTrack = null;
921
855
  };
922
856
  };
@@ -1113,8 +1047,9 @@ async function installOverlay(page, title) {
1113
1047
  }
1114
1048
  if (iconLink.href !== favicon) iconLink.href = favicon;
1115
1049
  // Hide/Show for #wg-panel - call after every panel.innerHTML refresh.
1116
- // Collapsed title shows "N / M · block" (compact progress). forceCollapsed
1117
- // is for auto-next navs / video - does not overwrite the user's Hide preference.
1050
+ // Collapsed title shows "N / M · block" (compact progress).
1051
+ // forceCollapsed:true = force mini (--mini / --video). Omit otherwise so
1052
+ // the user's Hide preference (localStorage) wins. Never pass false.
1118
1053
  window.__wgWirePanelChrome = (panel, storageKey, chromeTitle, opts) => {
1119
1054
  if (!panel) return;
1120
1055
  opts = opts || {};
@@ -1161,8 +1096,6 @@ async function installOverlay(page, title) {
1161
1096
  let hidden = false;
1162
1097
  if (opts.forceCollapsed === true) {
1163
1098
  hidden = true;
1164
- } else if (opts.forceCollapsed === false) {
1165
- hidden = false;
1166
1099
  } else {
1167
1100
  try {
1168
1101
  hidden = localStorage.getItem(storageKey) === "1";
@@ -1170,6 +1103,7 @@ async function installOverlay(page, title) {
1170
1103
  /* ignore */
1171
1104
  }
1172
1105
  }
1106
+ // force mini does not overwrite Hide preference in storage.
1173
1107
  apply(hidden, opts.forceCollapsed === true ? false : true);
1174
1108
  const toggle = panel.querySelector("[data-wg-toggle]");
1175
1109
  if (toggle && !toggle.dataset.wgWired) {
@@ -1211,6 +1145,8 @@ async function renderBeforeStep(page, info) {
1211
1145
  if (window.__wgHideCursor) window.__wgHideCursor();
1212
1146
  })
1213
1147
  .catch(() => {});
1148
+ // miniStepLabel is Node-only — compute before evaluate (browser has no closure).
1149
+ const payload = { ...info, stepLabel: miniStepLabel(info) };
1214
1150
  await page
1215
1151
  .evaluate((info) => {
1216
1152
  // Reused in place, not removed + recreated, every step - the old
@@ -1343,8 +1279,8 @@ async function renderBeforeStep(page, info) {
1343
1279
  }
1344
1280
  if (window.__wgWirePanelChrome) {
1345
1281
  window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
1346
- stepLabel: miniStepLabel(info),
1347
- forceCollapsed: info.forceCollapsed === true ? true : info.forceCollapsed === false ? false : undefined,
1282
+ stepLabel: info.stepLabel || "",
1283
+ forceCollapsed: info.forceCollapsed === true ? true : undefined,
1348
1284
  });
1349
1285
  }
1350
1286
  // Live, human-readable preview of what a MemKey's raw JSON will
@@ -1416,7 +1352,7 @@ async function renderBeforeStep(page, info) {
1416
1352
  if (auto) auto.style.display = cb.checked ? "" : "none";
1417
1353
  });
1418
1354
  }
1419
- }, info)
1355
+ }, payload)
1420
1356
  .catch(() => {});
1421
1357
  }
1422
1358
 
@@ -1653,18 +1589,10 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1653
1589
  }
1654
1590
  const z = h.zoom != null && Number(h.zoom) > 1 ? Number(h.zoom) : 1;
1655
1591
  const camMs = 700;
1656
- const followMouse =
1657
- h.followMouse !== undefined
1658
- ? !!h.followMouse
1659
- : h.follow !== undefined
1660
- ? !!h.follow
1661
- : z > 1;
1662
- // Track/zoom first, then park the demo cursor on the LIVE rect.
1663
- // Placing the cursor on a pre-zoom boundingBox leaves it stranded after
1664
- // body scale (html cursor is outside the transform).
1592
+ // Camera auto-centers on the ring target. Host mouse never steers.
1665
1593
  await page
1666
1594
  .evaluate(
1667
- ({ sel, label, tone, size, weight, focus, color, zoom, followMouse, camMs, allowFollow }) => {
1595
+ ({ sel, label, tone, size, weight, focus, color, zoom, camMs }) => {
1668
1596
  if (window.__wgStartHighlightTrack) {
1669
1597
  window.__wgStartHighlightTrack({
1670
1598
  sel,
@@ -1675,9 +1603,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1675
1603
  focus,
1676
1604
  color,
1677
1605
  zoom,
1678
- followMouse,
1679
1606
  camMs,
1680
- allowFollow,
1681
1607
  });
1682
1608
  }
1683
1609
  },
@@ -1690,11 +1616,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1690
1616
  focus: !!h.focus,
1691
1617
  color: h.color || h.labelColor || h.textColor || "",
1692
1618
  zoom: z,
1693
- followMouse,
1694
1619
  camMs,
1695
- // Follow only while zoomed-out-between-rings is OK. zoomOut:false
1696
- // means hold the frame for the upcoming click - no chase.
1697
- allowFollow: followMouse && h.zoomOut !== false,
1698
1620
  },
1699
1621
  )
1700
1622
  .catch(() => {});
@@ -1704,19 +1626,18 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1704
1626
  const waitMs = alreadyZoomed ? Math.min(camMs, 420) : camMs;
1705
1627
  await new Promise((res) => setTimeout(res, waitMs));
1706
1628
  if (h.cursor !== false) {
1707
- await page
1708
- .evaluate(({ sel, ms }) => {
1629
+ // Park Playwright real mouse + demo cursor on the LIVE centered target.
1630
+ const live = await page
1631
+ .evaluate((sel) => {
1709
1632
  const el = document.querySelector(sel);
1710
- if (!el || !window.__wgMoveCursorTo) return;
1711
- window.__wgCameraLock = true;
1633
+ if (!el) return null;
1712
1634
  const r = el.getBoundingClientRect();
1713
- window.__wgMoveCursorTo(r.left + r.width / 2, r.top + r.height / 2, ms, false);
1714
- setTimeout(() => {
1715
- window.__wgCameraLock = false;
1716
- }, ms + 40);
1717
- }, { sel: h.selector, ms: Math.min(280, camMs) })
1718
- .catch(() => {});
1719
- await new Promise((res) => setTimeout(res, Math.min(280, camMs)));
1635
+ return { x: r.left, y: r.top, width: r.width, height: r.height };
1636
+ }, h.selector)
1637
+ .catch(() => null);
1638
+ if (live) {
1639
+ await moveCursorTo(page, live, Math.min(280, camMs), { keepLock: true });
1640
+ }
1720
1641
  }
1721
1642
  // Appear settle
1722
1643
  await new Promise((res) => setTimeout(res, fadeMs));
@@ -1777,7 +1698,9 @@ async function renderAfterStep(page, info) {
1777
1698
  // 900ms / 200ms holds when set.
1778
1699
  await cycleHighlightRings(page, highlights, gatesFast, {
1779
1700
  pace: info.pace,
1701
+ stepLabel: miniStepLabel(info),
1780
1702
  });
1703
+ const afterPayload = { ...info, stepLabel: miniStepLabel(info) };
1781
1704
  await page
1782
1705
  .evaluate((info) => {
1783
1706
  // Reused in place - see renderBeforeStep's own comment on this.
@@ -1896,8 +1819,8 @@ async function renderAfterStep(page, info) {
1896
1819
  }
1897
1820
  if (window.__wgWirePanelChrome) {
1898
1821
  window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
1899
- stepLabel: miniStepLabel(info),
1900
- forceCollapsed: info.forceCollapsed === true ? true : info.forceCollapsed === false ? false : undefined,
1822
+ stepLabel: info.stepLabel || "",
1823
+ forceCollapsed: info.forceCollapsed === true ? true : undefined,
1901
1824
  });
1902
1825
  }
1903
1826
  panel.querySelectorAll(".wg-toggle-btn").forEach((btn) => {
@@ -1926,7 +1849,7 @@ async function renderAfterStep(page, info) {
1926
1849
  if (auto) auto.style.display = cb.checked ? "" : "none";
1927
1850
  });
1928
1851
  }
1929
- }, info)
1852
+ }, afterPayload)
1930
1853
  .catch(() => {});
1931
1854
  }
1932
1855
 
@@ -1938,6 +1861,7 @@ async function renderAfterStep(page, info) {
1938
1861
  */
1939
1862
  async function renderStepError(page, info) {
1940
1863
  await installOverlay(page, info.title);
1864
+ const errorPayload = { ...info, stepLabel: miniStepLabel(info) };
1941
1865
  await page
1942
1866
  .evaluate((info) => {
1943
1867
  // Reused in place - see renderBeforeStep's own comment on this.
@@ -1998,15 +1922,15 @@ async function renderStepError(page, info) {
1998
1922
  }
1999
1923
  if (window.__wgWirePanelChrome) {
2000
1924
  window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
2001
- stepLabel: miniStepLabel(info),
2002
- forceCollapsed: info.forceCollapsed === true ? true : info.forceCollapsed === false ? false : undefined,
1925
+ stepLabel: info.stepLabel || "",
1926
+ forceCollapsed: info.forceCollapsed === true ? true : undefined,
2003
1927
  });
2004
1928
  }
2005
1929
  const runBtn = document.getElementById("wg-run");
2006
1930
  if (runBtn) runBtn.addEventListener("click", () => window.__wgNext({}));
2007
1931
  const retryBtn = document.getElementById("wg-error-retry");
2008
1932
  if (retryBtn) retryBtn.addEventListener("click", () => window.__wgNext({ __wgRetry: "1" }));
2009
- }, info)
1933
+ }, errorPayload)
2010
1934
  .catch(() => {});
2011
1935
  }
2012
1936
 
@@ -2249,7 +2173,9 @@ async function presentSlides(page, slides, _gate, opts) {
2249
2173
  panel = document.createElement("div");
2250
2174
  panel.id = "wg-panel";
2251
2175
  }
2252
- panel.classList.remove("wg-collapsed", "wg-error", "wg-expected");
2176
+ panel.classList.remove("wg-error", "wg-expected");
2177
+ // Do not clear wg-collapsed here - WirePanelChrome honors Hide (localStorage)
2178
+ // unless forceCollapsed:true (--mini / --video).
2253
2179
  const esc = (t) =>
2254
2180
  String(t).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
2255
2181
  const episodeLine =
@@ -2328,7 +2254,8 @@ async function presentSlides(page, slides, _gate, opts) {
2328
2254
  " / " +
2329
2255
  info.total +
2330
2256
  (info.blockName ? " \\u00b7 " + info.blockName : ""),
2331
- forceCollapsed: false,
2257
+ // Video / --mini stays on the mini stepper (Ep N · step · block pill).
2258
+ forceCollapsed: info.forceCollapsed === true ? true : undefined,
2332
2259
  });
2333
2260
  }
2334
2261
  let autoNow = false;
@@ -2398,6 +2325,12 @@ async function presentSlides(page, slides, _gate, opts) {
2398
2325
  paceBadge: formatDemoPaceBadge(demoPace, baseAutoplay),
2399
2326
  paceLabel: formatDemoPaceLabel(demoPace, baseAutoplay),
2400
2327
  paceKind: demoPaceKind(demoPace),
2328
+ forceCollapsed:
2329
+ !!process.env.WAYGRAPH_VIDEO ||
2330
+ process.env.WAYGRAPH_MINI === "1" ||
2331
+ process.env.WAYGRAPH_STEPPER_MINI === "1"
2332
+ ? true
2333
+ : undefined,
2401
2334
  },
2402
2335
  );
2403
2336
  const started = Date.now();
@@ -2489,19 +2422,27 @@ async function moveCursorTo(page, box, ms, opts) {
2489
2422
  const y = box.y + box.height / 2;
2490
2423
  const travel = Math.max(0, Number(ms) || 0);
2491
2424
  const keepLock = !!(opts && opts.keepLock);
2492
- // Freeze camera follow while the demo cursor travels - otherwise dead-zone
2493
- // pans fight the click target (esp. right-edge cart chrome).
2425
+ // Camera stays locked; demo cursor + Playwright real mouse move together.
2426
+ // Host OS mouse is ignored (no mousemove camera steer).
2494
2427
  await page
2495
2428
  .evaluate(({ x, y, ms }) => {
2496
2429
  window.__wgCameraLock = true;
2430
+ window.__wgIgnoreHostMouse = true;
2497
2431
  if (window.__wgMoveCursorTo) window.__wgMoveCursorTo(x, y, ms);
2498
2432
  }, { x, y, ms: travel })
2499
2433
  .catch(() => {});
2500
- // CSS transition is async - wait the full travel so the cursor is ON the
2501
- // target before ring/pulse/real click (NavBlock click nav was skipping
2502
- // this and looked like a teleport).
2434
+ try {
2435
+ if (travel > 0) {
2436
+ const steps = Math.max(1, Math.min(30, Math.round(travel / 20)));
2437
+ await page.mouse.move(x, y, { steps });
2438
+ } else {
2439
+ await page.mouse.move(x, y);
2440
+ }
2441
+ } catch {
2442
+ /* headed / detached - demo cursor still moved */
2443
+ }
2503
2444
  if (travel > 0) {
2504
- await new Promise((res) => setTimeout(res, travel));
2445
+ await new Promise((res) => setTimeout(res, Math.min(80, travel)));
2505
2446
  }
2506
2447
  if (!keepLock) {
2507
2448
  await page
@@ -2513,6 +2454,16 @@ async function moveCursorTo(page, box, ms, opts) {
2513
2454
  return { x, y };
2514
2455
  }
2515
2456
 
2457
+ /** Live viewport center of a locator (after camera transform) - no scroll. */
2458
+ async function liveCenterOf(locator) {
2459
+ return locator
2460
+ .evaluate((el) => {
2461
+ const r = el.getBoundingClientRect();
2462
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2, w: r.width, h: r.height };
2463
+ })
2464
+ .catch(() => null);
2465
+ }
2466
+
2516
2467
  async function clickPulseAt(page, x, y, tone) {
2517
2468
  await page
2518
2469
  .evaluate(({ x, y, tone }) => {
@@ -2640,29 +2591,44 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2640
2591
  let keptCameraLock = false;
2641
2592
  try {
2642
2593
  await installOverlay(page);
2643
- // Wait until the target is visible BEFORE measuring - otherwise
2644
- // defineNavBlock({ click }) (and any slow-to-appear control) skips
2645
- // the cursor entirely: boundingBox was null, then originalClick
2646
- // waited and clicked with no demo animation.
2647
2594
  await this.waitFor({
2648
2595
  state: "visible",
2649
2596
  timeout: (options && options.timeout) || 30000,
2650
2597
  }).catch(() => {});
2651
- // Body-scale camera + Playwright scrollIntoView = teleport storm.
2652
- // Lock the camera for the whole click when zoomed; force-click so
2653
- // Playwright does not try to scroll the transformed page.
2654
2598
  const zoomed = await page
2655
2599
  .evaluate(() => !!(window.__wgCamera && window.__wgCamera.scale > 1.001))
2656
2600
  .catch(() => false);
2657
2601
  if (zoomed) {
2602
+ // Re-auto-center on this target, then lock - no host mouse, no scroll.
2658
2603
  await page
2659
2604
  .evaluate(() => {
2660
2605
  window.__wgCameraLock = true;
2606
+ window.__wgIgnoreHostMouse = true;
2661
2607
  })
2662
2608
  .catch(() => {});
2663
2609
  keptCameraLock = true;
2610
+ const handle = await this.elementHandle().catch(() => null);
2611
+ if (handle) {
2612
+ await page
2613
+ .evaluate(
2614
+ (el) => {
2615
+ if (!el || !window.__wgSetCamera || !window.__wgElDocPoint) return;
2616
+ const cam = window.__wgCamera || { scale: 1.4 };
2617
+ const pt = window.__wgElDocPoint(el);
2618
+ const s = cam.scale > 1.001 ? cam.scale : 1.4;
2619
+ window.__wgSetCamera(pt.x, pt.y, s, 280);
2620
+ },
2621
+ handle,
2622
+ )
2623
+ .catch(() => {});
2624
+ await handle.dispose().catch(() => {});
2625
+ await new Promise((res) => setTimeout(res, 300));
2626
+ }
2664
2627
  }
2665
- const box = await this.boundingBox();
2628
+ const live = await liveCenterOf(this);
2629
+ const box = live
2630
+ ? { x: live.x - live.w / 2, y: live.y - live.h / 2, width: live.w, height: live.h }
2631
+ : await this.boundingBox();
2666
2632
  const narrated = box ? await wasJustNarrated(page) : false;
2667
2633
  if (box && !narrated) {
2668
2634
  let fallback = "click";
@@ -2670,10 +2636,8 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2670
2636
  const text = (await this.textContent())?.trim();
2671
2637
  if (text && text.length > 0 && text.length <= 30) fallback = text;
2672
2638
  } catch {
2673
- // element has no simple text (an icon button, say) - generic label is fine
2639
+ /* ignore */
2674
2640
  }
2675
- // Prefer "nav: …" when this locator is a NavBlock click target
2676
- // (set on page by runStepMode just before act).
2677
2641
  try {
2678
2642
  const navLabel = await page.evaluate(() => window.__wgPendingNavClickLabel || null);
2679
2643
  if (navLabel) fallback = String(navLabel);
@@ -2683,13 +2647,13 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2683
2647
  const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
2684
2648
  const cap = await captionForLocator(page, this, stubs, fallback);
2685
2649
  clickTone = cap.tone;
2686
- // Already centered from stubBefore zoom: short cursor, keep lock.
2687
- const travel = zoomed ? cursorMs(160) : cursorMs(600);
2688
- clickPoint = await moveCursorTo(page, box, travel, {
2689
- keepLock: zoomed,
2690
- });
2691
- // Re-measure after cursor settle - stale pre-travel box teleports the ring.
2692
- const liveBox = (await this.boundingBox()) || box;
2650
+ const travel = zoomed ? cursorMs(200) : cursorMs(600);
2651
+ const again = (await liveCenterOf(this)) || live;
2652
+ const aim = again
2653
+ ? { x: again.x - again.w / 2, y: again.y - again.h / 2, width: again.w, height: again.h }
2654
+ : box;
2655
+ clickPoint = await moveCursorTo(page, aim, travel, { keepLock: zoomed });
2656
+ const liveBox = aim;
2693
2657
  clickPoint = {
2694
2658
  x: liveBox.x + liveBox.width / 2,
2695
2659
  y: liveBox.y + liveBox.height / 2,
@@ -2699,30 +2663,17 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2699
2663
  weight: cap.weight,
2700
2664
  });
2701
2665
  await dwellMatchedStub(page, this, stubs, pacing);
2702
- // "pop for a few seconds" - Dan's own phrase, matching the
2703
- // zsign demo-engine's ring-before-click pattern in
2704
- // services/help-center-clip-engine's video-pipeline.
2705
2666
  await new Promise((res) => setTimeout(res, clickPrePop()));
2706
2667
  } else if (box) {
2707
- // Ring/caption already handled by narrate() - still move the
2708
- // cursor + pulse the click point, just skip re-showing the ring.
2709
2668
  const travel = zoomed ? cursorMs(120) : cursorMs(200);
2710
- clickPoint = await moveCursorTo(page, box, travel, {
2711
- keepLock: zoomed,
2712
- });
2669
+ clickPoint = await moveCursorTo(page, box, travel, { keepLock: zoomed });
2713
2670
  clickTone = "planned";
2714
2671
  }
2715
2672
  } catch {
2716
- // best-effort - the real click below still runs either way
2673
+ /* best-effort */
2717
2674
  }
2718
2675
  if (clickPoint) {
2719
2676
  await clickPulseAt(page, clickPoint.x, clickPoint.y, clickTone);
2720
- // clickPulseAt only triggers the CSS animation class - it doesn't
2721
- // wait for it. Without a pause here, the real click (and any
2722
- // resulting navigation/DOM change) fires while the ripple is still
2723
- // mid-animation, sometimes cutting it off before it's even visible.
2724
- // Wait out the same .5s the "@keyframes wg-pulse" rule uses, so the
2725
- // mock click visually completes before the real one fires.
2726
2677
  await new Promise((res) => setTimeout(res, skipTheater() ? 0 : 500));
2727
2678
  }
2728
2679
  const zoomedClick = await page
@@ -2731,21 +2682,29 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2731
2682
  try {
2732
2683
  let result;
2733
2684
  if (zoomedClick || keptCameraLock) {
2734
- // Playwright scrollIntoView + body-scale camera = "outside viewport"
2735
- // retry storm. Click the element in-page with no scrolling.
2736
- const handle = await this.elementHandle().catch(() => null);
2737
- if (handle) {
2738
- await handle.evaluate((el) => {
2739
- if (el && typeof el.click === "function") el.click();
2740
- });
2741
- await handle.dispose().catch(() => {});
2685
+ // Real Playwright mouse at live visual center - never scrollIntoView.
2686
+ const pt =
2687
+ clickPoint ||
2688
+ (await liveCenterOf(this)) ||
2689
+ null;
2690
+ if (pt && Number.isFinite(pt.x) && Number.isFinite(pt.y)) {
2691
+ await page.mouse.click(pt.x, pt.y);
2742
2692
  result = undefined;
2743
2693
  } else {
2744
- result = await originalClick.call(this, {
2745
- ...(options || {}),
2746
- force: true,
2747
- timeout: (options && options.timeout) || 5000,
2748
- });
2694
+ const handle = await this.elementHandle().catch(() => null);
2695
+ if (handle) {
2696
+ await handle.evaluate((el) => {
2697
+ if (el && typeof el.click === "function") el.click();
2698
+ });
2699
+ await handle.dispose().catch(() => {});
2700
+ result = undefined;
2701
+ } else {
2702
+ result = await originalClick.call(this, {
2703
+ ...(options || {}),
2704
+ force: true,
2705
+ timeout: (options && options.timeout) || 5000,
2706
+ });
2707
+ }
2749
2708
  }
2750
2709
  } else {
2751
2710
  result = await originalClick.call(this, options);
@@ -3075,10 +3034,15 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3075
3034
  const stubBeforeTodos = stubBeforePhase.todos || [];
3076
3035
  const isNavBlock = r.block.__waygraphKind === "nav";
3077
3036
  const autoNow = await currentAutoplay();
3078
- // Video recording: keep chrome compact. Live auto-next used to force-collapse
3079
- // every NavBlock and hid the stepper (todos / modules) - only collapse while
3080
- // act() runs (markStepRunning) so Playwright clicks hit the page, not the panel.
3081
- const forceCollapsed = !!process.env.WAYGRAPH_VIDEO;
3037
+ // Video / --mini: compact "Ep N · step · block" pill only.
3038
+ // Otherwise omit (undefined) so Hide/localStorage is honored - never pass false.
3039
+ // WAYGRAPH_STEPPER_MINI=1 remains a compat env alias for WAYGRAPH_MINI.
3040
+ const forceCollapsed =
3041
+ !!process.env.WAYGRAPH_VIDEO ||
3042
+ process.env.WAYGRAPH_MINI === "1" ||
3043
+ process.env.WAYGRAPH_STEPPER_MINI === "1"
3044
+ ? true
3045
+ : undefined;
3082
3046
  const requires = r.block.requires ?? [];
3083
3047
  const keys = requires.map((k) => {
3084
3048
  let value = "<not yet set>";
@@ -3259,7 +3223,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3259
3223
  episodeTitle: r.episodeTitle,
3260
3224
  expectedFailureReason: r.expectedFailureReason,
3261
3225
  stepperMode,
3262
- forceCollapsed: false,
3226
+ // Omit forceCollapsed on errors so Hide preference is not stomped.
3263
3227
  });
3264
3228
  const errEdits = await gate();
3265
3229
  if (errEdits && errEdits.__wgRetry) {
@@ -3355,7 +3319,12 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3355
3319
  episodeTitle: r.episodeTitle,
3356
3320
  stepperMode,
3357
3321
  todos: stubAfterTodos,
3358
- forceCollapsed: !!process.env.WAYGRAPH_VIDEO,
3322
+ forceCollapsed:
3323
+ !!process.env.WAYGRAPH_VIDEO ||
3324
+ process.env.WAYGRAPH_MINI === "1" ||
3325
+ process.env.WAYGRAPH_STEPPER_MINI === "1"
3326
+ ? true
3327
+ : undefined,
3359
3328
  });
3360
3329
  await gate();
3361
3330
  }
@@ -4370,6 +4339,9 @@ function parseRunFlags(argv) {
4370
4339
  else if (a === "--full") {
4371
4340
  out.fullStepper = true;
4372
4341
  }
4342
+ else if (a === "--mini" || a === "--stepper-mini") {
4343
+ out.miniStepper = true;
4344
+ }
4373
4345
  else if (a === "--ff-expand") {
4374
4346
  out.ffExpand = true;
4375
4347
  }
@@ -4490,8 +4462,8 @@ function applyRunFlags(flags, opts) {
4490
4462
  process.env.WAYGRAPH_HEADED = "0";
4491
4463
  }
4492
4464
  }
4493
- if ((flags.fast || flags.fullStepper) && !opts?.allowDemoUi) {
4494
- console.error("waygraph: --fast / --full are demo-only flags");
4465
+ if ((flags.fast || flags.fullStepper || flags.miniStepper) && !opts?.allowDemoUi) {
4466
+ console.error("waygraph: --fast / --full / --mini are demo-only flags");
4495
4467
  process.exit(1);
4496
4468
  }
4497
4469
  if (flags.step === true) {
@@ -4535,6 +4507,9 @@ function applyRunFlags(flags, opts) {
4535
4507
  if (flags.fullStepper) {
4536
4508
  process.env.WAYGRAPH_STEPPER = "full";
4537
4509
  }
4510
+ if (flags.miniStepper) {
4511
+ process.env.WAYGRAPH_MINI = "1";
4512
+ }
4538
4513
  if (flags.ffExpand) {
4539
4514
  process.env.WAYGRAPH_FF_EXPAND = "1";
4540
4515
  }
@@ -4699,6 +4674,7 @@ Primary (less is more):
4699
4674
  --auto-next Auto-advance steps (alias: --autoplay)
4700
4675
  --fast Shorter auto-next / Next gates (keeps smooth cursor)
4701
4676
  --full Classic wrap-all block chips (default: carousel)
4677
+ --mini Force compact mini panel (Hide pill); alias --stepper-mini
4702
4678
  --ff-expand Expand fastForwardComposeBlock inners as separate steps
4703
4679
  --ff-disabled Dispute: expand FF (alias --no-ff); blitz kept on those inners
4704
4680
  --auto-play-video Unattended + recorded: --auto-next + --video (+ step); headless by default
@@ -5093,7 +5069,7 @@ Agents shipped: waygraph-planner, waygraph-author, waygraph-healer.
5093
5069
  console.error("waygraph demo: missing flow/spec — e.g.\n" +
5094
5070
  " waygraph demo src/flows/shop.flow.ts\n" +
5095
5071
  " waygraph demo --blocks shopFlow\n" +
5096
- " Flags: --blocks --data --auto-next --fast --full --ff-expand --ff-disabled --auto-play-video --title --base-url --video");
5072
+ " Flags: --blocks --data --auto-next --fast --full --mini --ff-expand --ff-disabled --auto-play-video --title --base-url --video");
5097
5073
  process.exit(1);
5098
5074
  }
5099
5075
  const proj = flags.blocks
@@ -97,12 +97,11 @@ export interface WaygraphHighlightStub extends FixtureDurationFields {
97
97
  */
98
98
  focus?: boolean;
99
99
  /**
100
- * After camera zoom settles, pan with the real mouse pointer (Screen Studio).
101
- * Default: on when `zoom` > 1. Set false to lock the camera on the target.
102
- * Alias: `follow`.
100
+ * Deprecated no-op. Camera auto-centers on the ring target; host mouse is
101
+ * ignored. Playwright's real mouse is driven to the centered target instead.
103
102
  */
104
103
  followMouse?: boolean;
105
- /** Alias for {@link WaygraphHighlightStub.followMouse}. */
104
+ /** Alias for {@link WaygraphHighlightStub.followMouse} (no-op). */
106
105
  follow?: boolean;
107
106
  /**
108
107
  * Caption text color (CSS color). Overrides tone label color when set.
@@ -49,7 +49,6 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
49
49
  color: "#c9a6ff",
50
50
  zoom: 1.45,
51
51
  zoomOut: false,
52
- followMouse: true,
53
52
  },
54
53
  add: {
55
54
  selector: InventorySel.addBtn(DEMO_PRODUCT_ID),
@@ -75,7 +74,6 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
75
74
  color: "#86efac",
76
75
  zoom: 1.6,
77
76
  zoomOut: false,
78
- followMouse: true,
79
77
  duration: true,
80
78
  });
81
79
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.39",
3
+ "version": "0.12.41",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -49,7 +49,6 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
49
49
  color: "#c9a6ff",
50
50
  zoom: 1.45,
51
51
  zoomOut: false,
52
- followMouse: true,
53
52
  },
54
53
  add: {
55
54
  selector: InventorySel.addBtn(DEMO_PRODUCT_ID),
@@ -75,7 +74,6 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
75
74
  color: "#86efac",
76
75
  zoom: 1.6,
77
76
  zoomOut: false,
78
- followMouse: true,
79
77
  duration: true,
80
78
  });
81
79
  },