waygraph 0.15.32 → 0.15.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/engine/core.js +5 -0
  2. package/dist/engine/engine-class.d.ts +37 -0
  3. package/dist/engine/engine-class.js +98 -0
  4. package/dist/highlights/run-phase.js +2 -0
  5. package/dist/highlights/stub-ctx.js +36 -2
  6. package/dist/highlights/types.d.ts +35 -3
  7. package/dist/index.d.ts +1 -1
  8. package/dist/pilot-overlay/fixtures.d.ts +9 -1
  9. package/dist/pilot-overlay/fixtures.js +38 -1
  10. package/dist/pilot-overlay/overlay-fixtures.d.ts +9 -0
  11. package/dist/pilot-overlay/overlay-fixtures.js +45 -0
  12. package/dist/pilot-overlay/shell.js +4 -23
  13. package/dist/pilot-overlay.d.ts +1 -0
  14. package/dist/pilot-overlay.js +1 -0
  15. package/dist/runner/banner-log.js +62 -0
  16. package/dist/runner/demo-log.js +9 -1
  17. package/dist/runner/inpage/banner.js +56 -1
  18. package/dist/runner/inpage/core.js +2 -0
  19. package/dist/runner/overlay-css.js +6 -27
  20. package/dist/runner/overlay-install.js +7 -4
  21. package/dist/runner/overlay-present.d.ts +8 -0
  22. package/dist/runner/overlay-present.js +57 -0
  23. package/dist/runner/overlay-stage.js +105 -0
  24. package/dist/runner/step-mode.js +35 -126
  25. package/dist/runner/step-panels.js +3 -3
  26. package/dist/step-overlay.js +1 -3
  27. package/dist/ui/overlay.css +209 -0
  28. package/dist/ui/shadow.d.ts +7 -2
  29. package/dist/ui/shadow.js +12 -4
  30. package/dist/ui/stylesheet.d.ts +7 -0
  31. package/dist/ui/stylesheet.js +18 -0
  32. package/dist/verbose-log.d.ts +1 -0
  33. package/dist/verbose-log.js +16 -0
  34. package/examples/saucedemo/.waygraph-auto/03df352d.log +0 -0
  35. package/examples/saucedemo/.waygraph-auto/0a8c3719.log +0 -0
  36. package/examples/saucedemo/.waygraph-auto/102c38ca.log +0 -0
  37. package/examples/saucedemo/.waygraph-auto/232a1d81.log +0 -0
  38. package/examples/saucedemo/.waygraph-auto/2553d107.log +0 -0
  39. package/examples/saucedemo/.waygraph-auto/2f4f531e.log +0 -0
  40. package/examples/saucedemo/.waygraph-auto/2fdbd916.log +0 -0
  41. package/examples/saucedemo/.waygraph-auto/331c6e8e.log +0 -0
  42. package/examples/saucedemo/.waygraph-auto/38f6799d.log +0 -0
  43. package/examples/saucedemo/.waygraph-auto/3bdb2f5a.log +0 -0
  44. package/examples/saucedemo/.waygraph-auto/5b838485.log +0 -0
  45. package/examples/saucedemo/.waygraph-auto/5d13bdde.log +0 -0
  46. package/examples/saucedemo/.waygraph-auto/63b0f4cd.log +0 -0
  47. package/examples/saucedemo/.waygraph-auto/6f05bdc1.log +0 -0
  48. package/examples/saucedemo/.waygraph-auto/70279fc3.log +0 -0
  49. package/examples/saucedemo/.waygraph-auto/731a14bd.log +0 -0
  50. package/examples/saucedemo/.waygraph-auto/76128a2d.log +0 -0
  51. package/examples/saucedemo/.waygraph-auto/796ab578.log +0 -0
  52. package/examples/saucedemo/.waygraph-auto/81257bf0.log +0 -0
  53. package/examples/saucedemo/.waygraph-auto/859a261d.log +0 -0
  54. package/examples/saucedemo/.waygraph-auto/a1ab5d29.log +0 -0
  55. package/examples/saucedemo/.waygraph-auto/a4b36275.log +0 -0
  56. package/examples/saucedemo/.waygraph-auto/a94738b7.log +0 -0
  57. package/examples/saucedemo/.waygraph-auto/b3d172f3.log +0 -0
  58. package/examples/saucedemo/.waygraph-auto/b5569336.log +0 -0
  59. package/examples/saucedemo/.waygraph-auto/b57921c5.log +0 -0
  60. package/examples/saucedemo/.waygraph-auto/b9ab0983.log +0 -0
  61. package/examples/saucedemo/.waygraph-auto/d37719a6.log +0 -0
  62. package/examples/saucedemo/.waygraph-auto/da0a06bf.log +0 -0
  63. package/examples/saucedemo/.waygraph-auto/dc58eb72.log +0 -0
  64. package/examples/saucedemo/.waygraph-auto/ff616932.log +0 -0
  65. package/package.json +3 -2
  66. package/examples/saucedemo/.waygraph-auto/535e8922.json +0 -8
@@ -1,10 +1,11 @@
1
1
  // In-page installer (runs inside the browser via page.evaluate): must stay self-contained - no module-scope closure.
2
2
  // Split out of the former single installOverlay evaluate (see src/ARCHITECTURE.md). Shares state only via window.__wg*.
3
3
  /* eslint-disable no-unused-vars */
4
- export function installBanner({ title, favicon, bannerPos, todoPos, envAutoplay, todoDockUi }) {
4
+ export function installBanner({ title, favicon, bannerPos, todoPos, envAutoplay, todoDockUi, bannerUi }) {
5
5
  const POSITIONS = ["left", "center", "right"];
6
6
  const applyPos = (el, pos) => {
7
7
  el.dataset.pos = pos;
8
+ el.dataset.home = pos;
8
9
  try {
9
10
  localStorage.setItem("wg-banner-pos", pos);
10
11
  } catch {
@@ -52,6 +53,60 @@ export function installBanner({ title, favicon, bannerPos, todoPos, envAutoplay,
52
53
  text.textContent = title;
53
54
  }
54
55
  }
56
+ // Authored banner UX (StubCtx.titlePos / bannerUi): applied every step, wins over click/env.
57
+ if (bannerUi) {
58
+ const cur = window.__wgBannerUi || {};
59
+ window.__wgBannerUi = { ...cur, ...bannerUi };
60
+ }
61
+ const bui = window.__wgBannerUi || {};
62
+ const bannerEl = __wgById("wg-banner");
63
+ if (bannerEl) {
64
+ if (bui.pos && POSITIONS.includes(bui.pos) && bannerEl.dataset.home !== bui.pos) {
65
+ bannerEl.dataset.pos = bui.pos;
66
+ bannerEl.dataset.home = bui.pos;
67
+ bannerEl.dataset.authored = "1";
68
+ }
69
+ bannerEl.dataset.collision = bui.collision === false ? "0" : "1";
70
+ bannerEl.style.display = bui.hidden ? "none" : "";
71
+ }
72
+ // Move the banner out of the way of a highlight ring (mirrors the todo dock's collision flip).
73
+ window.__wgBannerAvoidRing = (box) => {
74
+ const b = __wgById("wg-banner");
75
+ if (!b || !box || b.dataset.collision === "0" || b.style.display === "none") return;
76
+ const pad = 10;
77
+ const hits = () => {
78
+ const r = b.getBoundingClientRect();
79
+ return !(
80
+ r.right < box.x - pad ||
81
+ r.left > box.x + box.width + pad ||
82
+ r.bottom < box.y - pad ||
83
+ r.top > box.y + box.height + pad
84
+ );
85
+ };
86
+ const home = b.dataset.home || b.dataset.pos || "left";
87
+ b.dataset.pos = home;
88
+ if (!hits()) {
89
+ delete b.dataset.moved;
90
+ return;
91
+ }
92
+ for (const cand of POSITIONS.filter((p) => p !== home)) {
93
+ b.dataset.pos = cand;
94
+ if (!hits()) {
95
+ b.dataset.moved = home + ">" + cand;
96
+ (window.__wgBannerLog = window.__wgBannerLog || []).push("moved " + home + " -> " + cand + " (ring overlap)");
97
+ return;
98
+ }
99
+ }
100
+ b.dataset.pos = home;
101
+ b.dataset.moved = "none-free";
102
+ (window.__wgBannerLog = window.__wgBannerLog || []).push("overlap at every position (kept " + home + ")");
103
+ };
104
+ window.__wgBannerRestore = () => {
105
+ const b = __wgById("wg-banner");
106
+ if (!b) return;
107
+ if (b.dataset.home) b.dataset.pos = b.dataset.home;
108
+ delete b.dataset.moved;
109
+ };
55
110
  // Tab title/favicon: a real navigation resets document.title and any
56
111
  // <link rel="icon"> the new document brings, so re-check (not
57
112
  // re-append) on every call instead of a one-time flag.
@@ -114,6 +114,7 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
114
114
  ringLabel.style.top = labelTop + "px";
115
115
  if (window.__wgTodosSetBehind) window.__wgTodosSetBehind(true);
116
116
  if (window.__wgTodosAvoidRingCollision) window.__wgTodosAvoidRingCollision(box);
117
+ if (window.__wgBannerAvoidRing) window.__wgBannerAvoidRing(box);
117
118
  };
118
119
  /** Dim/lower floating todo docks while a ring caption is the focus. */
119
120
  window.__wgTodosSetBehind = (on) => {
@@ -220,6 +221,7 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
220
221
  if (ringLabel) ringLabel.style.opacity = "0";
221
222
  if (window.__wgClearFocus) window.__wgClearFocus();
222
223
  if (window.__wgTodosSetBehind) window.__wgTodosSetBehind(false);
224
+ if (window.__wgBannerRestore) window.__wgBannerRestore();
223
225
  window.__wgTodoCollisionLocked = false;
224
226
  __wgQA(".wg-todo-dock[data-tucked], #wg-todo-dock[data-tucked]").forEach((el) => {
225
227
  delete el.dataset.tucked;
@@ -1,7 +1,5 @@
1
1
  // Moved verbatim from the former CHAIN_RUNNER_SCRIPT template string in cli.ts (see src/ARCHITECTURE.md).
2
2
  // Runs inside the target project's own waygraph copy - keep it dependency-light and self-contained.
3
- import { WAYGRAPH_RING_CSS } from "../highlights.js";
4
- import { cursorCss } from "../ui/components/cursor.js";
5
3
  import { SURFACE } from "../ui/tokens.js";
6
4
 
7
5
  // ---------------------------------------------------------------------------
@@ -13,22 +11,11 @@ import { SURFACE } from "../ui/tokens.js";
13
11
  // label-under-box language, minus everything camera/narration-specific).
14
12
  // Purely CLI-level orchestration - no engine changes, no change to the
15
13
  // non-STEP path above.
16
- export const RING_CSS =
17
- // Shared with pilot-overlay.ts's showPilotVision - same ring/label CSS,
18
- // one source of truth (highlights.ts), so a Blind Pilot vision ring and a
19
- // demo stepper ring never visually drift apart.
20
- WAYGRAPH_RING_CSS +
21
- // Mouse cursor icon that travels to a target before it's acted on, plus a
22
- // quick expanding ripple at the moment of a click - same idea as
23
- // help-center-clip-engine's #clip-cursor/#clip-ring (video-pipeline). The
24
- // shape itself is an inline SVG set as innerHTML in installOverlay below
25
- // (dark fill + white stroke, same as the clip-engine's own cursor) - a
26
- // plain solid-white CSS clip-path (the first attempt here) had no outline
27
- // at all and all but disappeared against this app's light background.
28
- // Travel duration is JS-driven per call via --wg-cursor-ms, same reason
29
- // the clip engine's own comment gives: a hardcoded CSS duration would
30
- // make the speed param a no-op.
31
- cursorCss() +
14
+ // Runner-only chrome (step panel, todo-dock legacy variant, zoom/typing/device chips) - NOT
15
+ // ring/cursor/banner, which come from the one shared bundle every surface installs
16
+ // (ui/css/*.css, see ensureShadowRoot in overlay-install.js). Injected under its own key so it
17
+ // never collides with that bundle.
18
+ export const RUNNER_EXTRA_CSS =
32
19
  // Spotlight: dim everything except the highlight target (focus: true).
33
20
  "#wg-focus-veil{position:fixed;z-index:2147483644;pointer-events:none;" +
34
21
  "border-radius:12px;box-shadow:0 0 0 9999px rgba(8,4,20,.62);" +
@@ -225,15 +212,7 @@ export const RING_CSS =
225
212
  "#wg-modules .wg-mod-done{background:#2a1650;color:#9a7ad1;}" +
226
213
  "#wg-modules .wg-mod-current{background:#7C3AED;color:#fff;}" +
227
214
  "#wg-modules .wg-mod-upcoming{background:transparent;color:#5a4a80;border:1px solid #3a2a60;}" +
228
- "#wg-banner{position:fixed;z-index:2147483647;top:14px;max-width:320px;" +
229
- "background:" + SURFACE + ";color:#fff;border-radius:12px;padding:10px 16px;" +
230
- "font:14px/1.4 system-ui,sans-serif;box-shadow:0 8px 20px rgba(0,0,0,.3);" +
231
- "border:1px solid rgba(124,58,237,.4);cursor:pointer;user-select:none;}" +
232
- "#wg-banner[data-pos=left]{left:14px;right:auto;transform:none;}" +
233
- "#wg-banner[data-pos=center]{left:50%;right:auto;transform:translateX(-50%);}" +
234
- "#wg-banner[data-pos=right]{right:14px;left:auto;transform:none;}" +
235
- "#wg-banner .wg-banner-tag{display:block;font-size:10px;font-weight:700;color:#c9a6ff;" +
236
- "letter-spacing:.05em;text-transform:uppercase;margin-bottom:2px;}" +
215
+ // #wg-banner: comes from the shared bundle (ui/css/banner.css) now - not redefined here.
237
216
  "#wg-panel .wg-key{margin:8px 0;}" +
238
217
  // A live, human-readable preview of what's about to be written for this
239
218
  // MemKey - Dan: "i want pretty to exist in memkeys too... it shows what
@@ -1,6 +1,6 @@
1
1
  // Moved verbatim from the former CHAIN_RUNNER_SCRIPT template string in cli.ts (see src/ARCHITECTURE.md).
2
2
  // Runs inside the target project's own waygraph copy - keep it dependency-light and self-contained.
3
- import { HOST_CSS, RING_CSS, WAYGRAPH_FAVICON } from "./overlay-css.js";
3
+ import { HOST_CSS, RUNNER_EXTRA_CSS, WAYGRAPH_FAVICON } from "./overlay-css.js";
4
4
  import { ensureShadowRoot } from "../ui/shadow.js";
5
5
  import { resolveTodoDockUi } from "../highlights.js";
6
6
  import { installCore } from "./inpage/core.js";
@@ -12,9 +12,12 @@ import { installTodos } from "./inpage/todos.js";
12
12
  import { installDevice } from "./inpage/device.js";
13
13
  import { applyVideoDeviceStage } from "./device-stage.js";
14
14
 
15
- export async function installOverlay(page, title) {
15
+ /** `ui` = authored banner UX from the stub phase: { pos, collision, hidden } (all optional). */
16
+ export async function installOverlay(page, title, ui) {
17
+ // ring/cursor/banner/dock come from the ONE shared bundle every surface installs (ui/css/*.css).
16
18
  await ensureShadowRoot(page);
17
- await page.evaluate(([css, key]) => __wgCss(css, key), [RING_CSS, "wg-overlay-css"]).catch(() => {});
19
+ // Runner-only chrome (step panel, zoom/typing/device chips) - separate key, no collision.
20
+ await page.evaluate(([css, key]) => __wgCss(css, key), [RUNNER_EXTRA_CSS, "wg-runner-css"]).catch(() => {});
18
21
  await page.addStyleTag({ content: HOST_CSS }).catch(() => {});
19
22
  // Default top-left; override with WAYGRAPH_TITLE_POS=left|center|right.
20
23
  // Click cycles left -> center -> right (persisted in localStorage so a
@@ -25,7 +28,7 @@ export async function installOverlay(page, title) {
25
28
  const todoPos = envTodoPos === "right" ? "right" : "left";
26
29
  const envAutoplay = process.env.WAYGRAPH_AUTOPLAY === "1";
27
30
  const todoDockUi = resolveTodoDockUi();
28
- const args = { title, favicon: WAYGRAPH_FAVICON, bannerPos, todoPos, envAutoplay, todoDockUi };
31
+ const args = { title, favicon: WAYGRAPH_FAVICON, bannerPos, todoPos, envAutoplay, todoDockUi, bannerUi: ui || null };
29
32
  await page.evaluate(installCore, args).catch(() => {});
30
33
  await page.evaluate(installSwipeZoom, args).catch(() => {});
31
34
  await page.evaluate(installBanner, args).catch(() => {});
@@ -0,0 +1,8 @@
1
+ import type { Page } from "@playwright/test";
2
+ import type { StubPhaseResult } from "../highlights/types.js";
3
+
4
+ export interface SessionOverlay {
5
+ present(tag: string, phase: Partial<StubPhaseResult>): Promise<void>;
6
+ restore(): Promise<void>;
7
+ }
8
+ export function createSessionOverlay(page: Page, surface: string): SessionOverlay;
@@ -0,0 +1,57 @@
1
+ // Presents a resolved stub phase ("waygraph overlay fixtures": ctx.todos / device / title / banner ...) on a LIVE page
2
+ // for the session surfaces (auto / browser / pilot). Same OverlayStage + same in-page installers + same verbose log
3
+ // lines as `waygraph demo`, so an overlay fixture behaves identically on every surface. Rings are painted separately
4
+ // (pilot holds them; demo cycles them) - that is the only intended difference.
5
+ import { installOverlay } from "./overlay-install.js";
6
+ import { createOverlayStage } from "./overlay-stage.js";
7
+ import { demoLog, logStubPhaseFixtures, logTodoDockFull, setDemoLogSurface, summarizeDevice } from "./demo-log.js";
8
+ import { logBannerDom, probeBanner } from "./banner-log.js";
9
+ import { probeTodoDocksOnPage } from "./todo-dock.js";
10
+
11
+ export function createSessionOverlay(page, surface) {
12
+ setDemoLogSurface(surface);
13
+ const stage = createOverlayStage(page, { todoDockRef: { current: undefined }, deviceRef: { current: undefined } });
14
+ let title = undefined;
15
+ let painted = false;
16
+
17
+ const paintChrome = async (phaseParallel) => {
18
+ await installOverlay(page, title, stage.state.lastBannerUi);
19
+ await stage.syncAllTodoDocks({ parallel: !!phaseParallel });
20
+ };
21
+
22
+ return {
23
+ stage,
24
+ /** Apply one phase (tag = before | after | error | highlight) and log the resulting overlay state. */
25
+ async present(tag, phase) {
26
+ setDemoLogSurface(surface);
27
+ logStubPhaseFixtures(tag, phase);
28
+ await stage.pushTodoDockUi(phase.todoDockUi);
29
+ const { appliedTodo, appliedDevice } = await stage.applyPhase(phase, { reapplyDeviceOnKeep: true });
30
+ if (phase.title && String(phase.title).trim()) title = String(phase.title).trim();
31
+ const wantsChrome = !!(title || stage.state.lastTodoDock || phase.todoSync === "set");
32
+ if (wantsChrome) {
33
+ painted = true;
34
+ await paintChrome(phase.todoParallel);
35
+ }
36
+ logTodoDockFull(stage.state.lastTodoDock, " " + tag);
37
+ demoLog(" " + tag + " device=" + summarizeDevice(stage.state.lastDevice) + " deviceSync=" + appliedDevice.sync);
38
+ demoLog(" " + tag + " todoSync=" + appliedTodo.sync);
39
+ const docks = await probeTodoDocksOnPage(page);
40
+ if (!docks.length && stage.state.lastTodoDock && appliedTodo.sync !== "clear") {
41
+ demoLog(" WARN " + tag + ": todos authored but dock-dom empty (not painted?)");
42
+ }
43
+ for (const d of docks) {
44
+ demoLog(
45
+ " " + tag + " dock-dom " + d.key + "@" + d.pos + " " + d.left + "," + d.top + " " + d.w + "x" + d.h +
46
+ (d.onScreen ? "" : " OFFSCREEN") + " n=" + d.n + (d.current ? ' cur="' + d.current + '"' : ""),
47
+ );
48
+ }
49
+ if (title || painted) logBannerDom(tag, await probeBanner(page));
50
+ },
51
+ /** A navigation wipes the overlay DOM: rebuild what was authored (same job as the demo's post-navigation restore). */
52
+ async restore() {
53
+ if (!painted) return;
54
+ await paintChrome(false);
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,105 @@
1
+ // The waygraph OVERLAY stage: one owner of overlay state (todo docks, device, banner UX) and of how a resolved
2
+ // stub phase is applied to the page. demo / auto / browser / pilot all go through this, so a "waygraph overlay
3
+ // fixture" (ctx.todos / ctx.device / ctx.title / ...) means the same thing on every surface.
4
+ // NOT Playwright test fixtures - these paint the waygraph overlay on the page under test.
5
+ import { applyDevicePhase, applyTodoPhase, resolveTodoDockUi } from "../highlights.js";
6
+ import { applyDeviceToPage } from "./device-stage.js";
7
+ import { bannerUiFromPhase } from "./banner-log.js";
8
+
9
+ export function createOverlayStage(page, { todoDockRef, deviceRef }) {
10
+ /** Carried across steps: omitting a fixture in a later phase = keep (todos, device, banner UX). */
11
+ const state = { lastTodoDock: undefined, lastDevice: undefined, lastBannerUi: {} };
12
+ /** Author todoDockUi patches (merged); resolved against env each step. */
13
+ let lastTodoDockUiPatch = undefined;
14
+ /** Multi-todo: every dock keyed by todoId (or "_default"), survives navigation. */
15
+ const todoDockById = new Map();
16
+
17
+ const pushTodoDockUi = async (patch) => {
18
+ if (patch && typeof patch === "object") {
19
+ lastTodoDockUiPatch = { ...(lastTodoDockUiPatch || {}), ...patch };
20
+ }
21
+ const ui = resolveTodoDockUi(lastTodoDockUiPatch);
22
+ await page
23
+ .evaluate((u) => {
24
+ window.__wgTodoDockUi = u;
25
+ }, ui)
26
+ .catch(() => {});
27
+ };
28
+ const dockRegistryKey = (dock) =>
29
+ dock && dock.id && String(dock.id).trim() ? String(dock.id).trim() : "_default";
30
+ const rememberTodoDock = (dock, sync, parallel) => {
31
+ if (sync === "clear") {
32
+ todoDockById.clear();
33
+ return;
34
+ }
35
+ if (sync === "set" && dock) {
36
+ if (!parallel) todoDockById.clear();
37
+ todoDockById.set(dockRegistryKey(dock), dock);
38
+ return;
39
+ }
40
+ // keep: refresh / re-seed so syncAllTodoDocks never sees an empty map
41
+ // while lastTodoDock still carries (nav wipe + mid-act advance).
42
+ if (sync === "keep" && dock) {
43
+ todoDockById.set(dockRegistryKey(dock), dock);
44
+ }
45
+ };
46
+ const syncAllTodoDocks = async (opts) => {
47
+ const docks = [...todoDockById.values()];
48
+ const parallel = !!(opts && opts.parallel);
49
+ if (!docks.length) {
50
+ // Empty registry: do NOT clear the DOM. renderBefore/After may have
51
+ // just painted from lastTodoDock (keep). Clearing here made todos
52
+ // vanish until the next ctx.todos() set (PIA #15).
53
+ return;
54
+ }
55
+ await page
56
+ .evaluate(
57
+ ({ list, parallel }) => {
58
+ if (!window.__wgSyncTodos) return;
59
+ for (let i = 0; i < list.length; i++) {
60
+ const d = list[i];
61
+ window.__wgSyncTodos({
62
+ sync: "set",
63
+ dock: d,
64
+ todoId: (d && d.id) || null,
65
+ pos: (d && d.pos) || null,
66
+ // First dock replaces; later ones keep siblings when parallel.
67
+ replace: !(parallel && i > 0),
68
+ parallel: parallel && i > 0,
69
+ });
70
+ }
71
+ },
72
+ { list: docks, parallel },
73
+ )
74
+ .catch(() => {});
75
+ };
76
+
77
+ /**
78
+ * Merge one resolved stub phase (banner UX, todo dock, device) into the carried state and push it to the page.
79
+ * `reapplyDeviceOnKeep`: also re-apply the carried device when the phase says keep (after a navigation wipe).
80
+ */
81
+ const applyPhase = async (phase, { reapplyDeviceOnKeep = false } = {}) => {
82
+ state.lastBannerUi = { ...state.lastBannerUi, ...bannerUiFromPhase(phase) };
83
+ const appliedTodo = applyTodoPhase(state.lastTodoDock, {
84
+ todoSync: phase.todoSync,
85
+ todoDock: phase.todoDock,
86
+ todos: phase.todos,
87
+ todoPos: phase.todoPos,
88
+ });
89
+ state.lastTodoDock = appliedTodo.dock;
90
+ todoDockRef.current = state.lastTodoDock;
91
+ rememberTodoDock(appliedTodo.dock, appliedTodo.sync, !!phase.todoParallel);
92
+ const appliedDevice = applyDevicePhase(state.lastDevice, {
93
+ deviceSync: phase.deviceSync,
94
+ device: phase.device,
95
+ });
96
+ state.lastDevice = appliedDevice.device;
97
+ deviceRef.current = state.lastDevice;
98
+ if (appliedDevice.sync !== "keep" || (reapplyDeviceOnKeep && state.lastDevice)) {
99
+ await applyDeviceToPage(page, state.lastDevice, appliedDevice.sync);
100
+ }
101
+ return { appliedTodo, appliedDevice };
102
+ };
103
+
104
+ return { state, pushTodoDockUi, rememberTodoDock, syncAllTodoDocks, applyPhase };
105
+ }
@@ -1,12 +1,13 @@
1
1
  // Moved verbatim from the former CHAIN_RUNNER_SCRIPT template string in cli.ts (see src/ARCHITECTURE.md).
2
2
  // Runs inside the target project's own waygraph copy - keep it dependency-light and self-contained.
3
3
  import { instrumentInteractionHighlighting } from "./interaction-patch.js";
4
- import { applyDevicePhase, applyHighlightStyleDefaults, applyTodoPhase, demoPaceGateMs, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, formatHighlightCaption, hasAuthoredStubAfter, resolveSlides, resolveStepDemoPace, resolveTodoDockUi, runStubPhase } from "../highlights.js";
4
+ import { applyHighlightStyleDefaults, demoPaceGateMs, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, formatHighlightCaption, hasAuthoredStubAfter, resolveSlides, resolveStepDemoPace, resolveTodoDockUi, runStubPhase } from "../highlights.js";
5
5
  import { markStepRunning, resetPageState, teardownOverlay } from "./teardown.js";
6
6
  import { demoLog, logStubPhaseFixtures, logTodoDockFull, paceSpeakFields, summarizeDevice } from "./demo-log.js";
7
- import { applyDeviceToPage } from "./device-stage.js";
8
7
  import { extractVerifyHighlights, presentFailPanel, renderAfterStep, renderBeforeStep } from "./step-panels.js";
9
8
  import { probeTodoDocksOnPage } from "./todo-dock.js";
9
+ import { logBannerDom, probeBanner } from "./banner-log.js";
10
+ import { createOverlayStage } from "./overlay-stage.js";
10
11
  import { join } from "node:path";
11
12
  import { presentSlides } from "./slides.js";
12
13
 
@@ -26,9 +27,9 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
26
27
  const stepperMode = process.env.WAYGRAPH_STEPPER === "full" ? "full" : "carousel";
27
28
  const pacing = { gatesFast: demoFast, gatesSlow: false, skipTheater: false, demoPace: "normal" };
28
29
  const stubBeforeRef = { current: [] };
29
- /** Live todo dock for Method fill/click advance (same carry as lastTodoDock). */
30
+ /** Live todo dock for Method fill/click advance (same carry as stage.state.lastTodoDock). */
30
31
  const todoDockRef = { current: undefined };
31
- /** Live device fixture for touch theater (same carry as lastDevice). */
32
+ /** Live device fixture for touch theater (same carry as stage.state.lastDevice). */
32
33
  const deviceRef = { current: undefined };
33
34
  instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef, todoDockRef, deviceRef);
34
35
  // Force the panel checkbox from this process's flags/env at run start.
@@ -158,73 +159,8 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
158
159
  }
159
160
 
160
161
  let result;
161
- /** Carry floating todo dock across steps (empty stubBefore must not wipe). */
162
- let lastTodoDock = undefined;
163
- /** Author todoDockUi patches (merged); resolved against env each step. */
164
- let lastTodoDockUiPatch = undefined;
165
- /** Multi-todo: every dock keyed by todoId (or "_default"), survives navigation. */
166
- const todoDockById = new Map();
167
- const pushTodoDockUi = async (patch) => {
168
- if (patch && typeof patch === "object") {
169
- lastTodoDockUiPatch = { ...(lastTodoDockUiPatch || {}), ...patch };
170
- }
171
- const ui = resolveTodoDockUi(lastTodoDockUiPatch);
172
- await page
173
- .evaluate((u) => {
174
- window.__wgTodoDockUi = u;
175
- }, ui)
176
- .catch(() => {});
177
- };
178
- const dockRegistryKey = (dock) =>
179
- dock && dock.id && String(dock.id).trim() ? String(dock.id).trim() : "_default";
180
- const rememberTodoDock = (dock, sync, parallel) => {
181
- if (sync === "clear") {
182
- todoDockById.clear();
183
- return;
184
- }
185
- if (sync === "set" && dock) {
186
- if (!parallel) todoDockById.clear();
187
- todoDockById.set(dockRegistryKey(dock), dock);
188
- return;
189
- }
190
- // keep: refresh / re-seed so syncAllTodoDocks never sees an empty map
191
- // while lastTodoDock still carries (nav wipe + mid-act advance).
192
- if (sync === "keep" && dock) {
193
- todoDockById.set(dockRegistryKey(dock), dock);
194
- }
195
- };
196
- const syncAllTodoDocks = async (opts) => {
197
- const docks = [...todoDockById.values()];
198
- const parallel = !!(opts && opts.parallel);
199
- if (!docks.length) {
200
- // Empty registry: do NOT clear the DOM. renderBefore/After may have
201
- // just painted from lastTodoDock (keep). Clearing here made todos
202
- // vanish until the next ctx.todos() set (PIA #15).
203
- return;
204
- }
205
- await page
206
- .evaluate(
207
- ({ list, parallel }) => {
208
- if (!window.__wgSyncTodos) return;
209
- for (let i = 0; i < list.length; i++) {
210
- const d = list[i];
211
- window.__wgSyncTodos({
212
- sync: "set",
213
- dock: d,
214
- todoId: (d && d.id) || null,
215
- pos: (d && d.pos) || null,
216
- // First dock replaces; later ones keep siblings when parallel.
217
- replace: !(parallel && i > 0),
218
- parallel: parallel && i > 0,
219
- });
220
- }
221
- },
222
- { list: docks, parallel },
223
- )
224
- .catch(() => {});
225
- };
226
- /** Carry device / touch fixture across steps (omit = keep, like todos). */
227
- let lastDevice = undefined;
162
+ const stage = createOverlayStage(page, { todoDockRef, deviceRef });
163
+ const { pushTodoDockUi, rememberTodoDock, syncAllTodoDocks } = stage;
228
164
  for (let i = 0; i < resolved.length; i++) {
229
165
  const r = resolved[i];
230
166
  // The block breadcrumb is scoped to THIS step's own episode, not the
@@ -322,26 +258,12 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
322
258
  label: formatHighlightCaption(h),
323
259
  })),
324
260
  });
325
- const appliedBefore = applyTodoPhase(lastTodoDock, {
326
- todoSync: stubBeforePhase.todoSync,
327
- todoDock: stubBeforePhase.todoDock,
328
- todos: stubBeforePhase.todos,
329
- todoPos: stubBeforePhase.todoPos,
330
- });
331
- lastTodoDock = appliedBefore.dock;
332
- todoDockRef.current = lastTodoDock;
333
- rememberTodoDock(appliedBefore.dock, appliedBefore.sync, !!stubBeforePhase.todoParallel);
334
- const appliedDeviceBefore = applyDevicePhase(lastDevice, {
335
- deviceSync: stubBeforePhase.deviceSync,
336
- device: stubBeforePhase.device,
337
- });
338
- lastDevice = appliedDeviceBefore.device;
339
- deviceRef.current = lastDevice;
340
- if (appliedDeviceBefore.sync !== "keep" || lastDevice) {
341
- await applyDeviceToPage(page, lastDevice, appliedDeviceBefore.sync);
342
- }
261
+ const { appliedTodo: appliedBefore, appliedDevice: appliedDeviceBefore } = await stage.applyPhase(
262
+ stubBeforePhase,
263
+ { reapplyDeviceOnKeep: true },
264
+ );
343
265
  const stubBeforeTodos =
344
- (lastTodoDock && lastTodoDock.groups[0] && lastTodoDock.groups[0].items) ||
266
+ (stage.state.lastTodoDock && stage.state.lastTodoDock.groups[0] && stage.state.lastTodoDock.groups[0].items) ||
345
267
  stubBeforePhase.todos ||
346
268
  [];
347
269
  const overlayTitle =
@@ -350,7 +272,7 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
350
272
  title ||
351
273
  "waygraph demo";
352
274
  const overlayTodoPos =
353
- (lastTodoDock && lastTodoDock.pos) || stubBeforePhase.todoPos || undefined;
275
+ (stage.state.lastTodoDock && stage.state.lastTodoDock.pos) || stubBeforePhase.todoPos || undefined;
354
276
  const isNavBlock = r.block.__waygraphKind === "nav";
355
277
  const autoNow = await currentAutoplay();
356
278
  // Video / --mini: compact pill. Never pass false - Hide/localStorage wins.
@@ -391,7 +313,7 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
391
313
  " gatesFast=" +
392
314
  !!pacing.gatesFast,
393
315
  );
394
- demoLog(" " + summarizeDevice(lastDevice));
316
+ demoLog(" " + summarizeDevice(stage.state.lastDevice));
395
317
  const phaseZoom =
396
318
  stubBeforePhase.zoom != null && Number(stubBeforePhase.zoom) > 0
397
319
  ? Number(stubBeforePhase.zoom)
@@ -404,7 +326,7 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
404
326
  (phaseZoomOut != null ? " zoomOut=" + phaseZoomOut : "") +
405
327
  " (StubCtx / API; live chip top-left when ring zoom >1)",
406
328
  );
407
- logTodoDockFull(lastTodoDock, " before");
329
+ logTodoDockFull(stage.state.lastTodoDock, " before");
408
330
  demoLog(" todoSync=" + appliedBefore.sync);
409
331
  }
410
332
  await renderBeforeStep(page, {
@@ -419,22 +341,23 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
419
341
  allEpisodes,
420
342
  justEnteredEpisode,
421
343
  title: overlayTitle,
344
+ bannerUi: stage.state.lastBannerUi,
422
345
  todoPos: overlayTodoPos,
423
346
  episodeNumber: r.episodeNumber,
424
347
  episodeTitle: r.episodeTitle,
425
348
  stepperMode,
426
349
  forceCollapsed,
427
350
  todos: stubBeforeTodos,
428
- todoDock: lastTodoDock,
351
+ todoDock: stage.state.lastTodoDock,
429
352
  todoSync: appliedBefore.sync,
430
- todoId: lastTodoDock && lastTodoDock.id,
353
+ todoId: stage.state.lastTodoDock && stage.state.lastTodoDock.id,
431
354
  ...paceSpeak,
432
355
  });
433
356
  // Re-paint dock(s) - renderBeforeStep syncs the active one; this enforces replace.
434
357
  await syncAllTodoDocks({ parallel: !!stubBeforePhase.todoParallel });
435
358
  if (process.env.WAYGRAPH_JSON !== "1") {
436
359
  const docks = await probeTodoDocksOnPage(page);
437
- if (!docks.length && lastTodoDock) {
360
+ if (!docks.length && stage.state.lastTodoDock) {
438
361
  demoLog(" WARN todos authored but dock-dom empty (not painted?)");
439
362
  } else if (docks.length) {
440
363
  demoLog(
@@ -462,6 +385,7 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
462
385
  );
463
386
  }
464
387
  }
388
+ if (process.env.WAYGRAPH_JSON !== "1") logBannerDom("before", await probeBanner(page));
465
389
  const edits = await gate();
466
390
  for (const k of requires) {
467
391
  if (edits[k.name] !== undefined) {
@@ -554,9 +478,9 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
554
478
  .catch(() => {});
555
479
  result = stepOutcome.result;
556
480
  // Mid-act Method fill/click advances live on todoDockRef - fold that
557
- // back into lastTodoDock before stubAfter / next-block keep, or the
481
+ // back into stage.state.lastTodoDock before stubAfter / next-block keep, or the
558
482
  // next step reverts to the pre-act (blank / index-0) checklist.
559
- if (todoDockRef.current) lastTodoDock = todoDockRef.current;
483
+ if (todoDockRef.current) stage.state.lastTodoDock = todoDockRef.current;
560
484
  // withExpectedFailure last block that SUCCEEDS on the intentional fail
561
485
  // branch (e.g. submit-login -> LoginPage + error banner). Still show
562
486
  // stubOnError rings + amber "expected outcome" panel - branching no
@@ -621,8 +545,8 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
621
545
  let stubAfterTodos = [];
622
546
  let afterOverlayTitle = overlayTitle;
623
547
  let afterTodoPos = overlayTodoPos;
624
- let appliedAfter = { dock: lastTodoDock, sync: "keep" };
625
- let appliedDeviceAfter = { device: lastDevice, sync: "keep" };
548
+ let appliedAfter = { dock: stage.state.lastTodoDock, sync: "keep" };
549
+ let appliedDeviceAfter = { device: stage.state.lastDevice, sync: "keep" };
626
550
  let afterTodoParallel = false;
627
551
  if (hasAuthoredStubAfter(r.block, result, fixturesAfter)) {
628
552
  const afterPhase = await runStubPhase(r.block, "stubAfter", {
@@ -641,33 +565,16 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
641
565
  }),
642
566
  });
643
567
  afterTodoParallel = !!afterPhase.todoParallel;
644
- appliedAfter = applyTodoPhase(lastTodoDock, {
645
- todoSync: afterPhase.todoSync,
646
- todoDock: afterPhase.todoDock,
647
- todos: afterPhase.todos,
648
- todoPos: afterPhase.todoPos,
649
- });
650
- lastTodoDock = appliedAfter.dock;
651
- todoDockRef.current = lastTodoDock;
652
- rememberTodoDock(appliedAfter.dock, appliedAfter.sync, afterTodoParallel);
653
- appliedDeviceAfter = applyDevicePhase(lastDevice, {
654
- deviceSync: afterPhase.deviceSync,
655
- device: afterPhase.device,
656
- });
657
- lastDevice = appliedDeviceAfter.device;
658
- deviceRef.current = lastDevice;
659
- if (appliedDeviceAfter.sync !== "keep" || (appliedDeviceAfter.sync === "set" && lastDevice)) {
660
- await applyDeviceToPage(page, lastDevice, appliedDeviceAfter.sync);
661
- }
568
+ ({ appliedTodo: appliedAfter, appliedDevice: appliedDeviceAfter } = await stage.applyPhase(afterPhase));
662
569
  stubAfterTodos =
663
- (lastTodoDock && lastTodoDock.groups[0] && lastTodoDock.groups[0].items) ||
570
+ (stage.state.lastTodoDock && stage.state.lastTodoDock.groups[0] && stage.state.lastTodoDock.groups[0].items) ||
664
571
  afterPhase.todos ||
665
572
  [];
666
573
  if (afterPhase.title && String(afterPhase.title).trim()) {
667
574
  afterOverlayTitle = String(afterPhase.title).trim();
668
575
  }
669
576
  if (afterPhase.todoPos) afterTodoPos = afterPhase.todoPos;
670
- if (lastTodoDock && lastTodoDock.pos) afterTodoPos = lastTodoDock.pos;
577
+ if (stage.state.lastTodoDock && stage.state.lastTodoDock.pos) afterTodoPos = stage.state.lastTodoDock.pos;
671
578
  highlights = afterPhase.highlights.map((h) => {
672
579
  const styled = applyHighlightStyleDefaults(h, r.highlightStyle);
673
580
  return {
@@ -707,14 +614,15 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
707
614
  moduleIndex,
708
615
  allEpisodes,
709
616
  title: afterOverlayTitle,
617
+ bannerUi: stage.state.lastBannerUi,
710
618
  todoPos: afterTodoPos,
711
619
  episodeNumber: r.episodeNumber,
712
620
  episodeTitle: r.episodeTitle,
713
621
  stepperMode,
714
622
  todos: stubAfterTodos,
715
- todoDock: lastTodoDock,
623
+ todoDock: stage.state.lastTodoDock,
716
624
  todoSync: appliedAfter.sync,
717
- todoId: lastTodoDock && lastTodoDock.id,
625
+ todoId: stage.state.lastTodoDock && stage.state.lastTodoDock.id,
718
626
  todoDockRef,
719
627
  forceCollapsed:
720
628
  !!process.env.WAYGRAPH_VIDEO ||
@@ -726,20 +634,20 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
726
634
  await syncAllTodoDocks({ parallel: afterTodoParallel });
727
635
  // Ring cycle / stubAfter may have advanced the dock - persist for next block.
728
636
  if (todoDockRef.current) {
729
- lastTodoDock = todoDockRef.current;
637
+ stage.state.lastTodoDock = todoDockRef.current;
730
638
  rememberTodoDock(todoDockRef.current, "set", afterTodoParallel);
731
639
  }
732
640
  if (process.env.WAYGRAPH_JSON !== "1") {
733
641
  demoLog(
734
642
  " after " +
735
- summarizeDevice(lastDevice) +
643
+ summarizeDevice(stage.state.lastDevice) +
736
644
  " deviceSync=" +
737
645
  appliedDeviceAfter.sync,
738
646
  );
739
- logTodoDockFull(lastTodoDock, " after");
647
+ logTodoDockFull(stage.state.lastTodoDock, " after");
740
648
  demoLog(" after todoSync=" + appliedAfter.sync);
741
649
  const docksAfter = await probeTodoDocksOnPage(page);
742
- if (!docksAfter.length && lastTodoDock && appliedAfter.sync !== "clear") {
650
+ if (!docksAfter.length && stage.state.lastTodoDock && appliedAfter.sync !== "clear") {
743
651
  demoLog(" WARN after: todos authored but dock-dom empty");
744
652
  } else if (docksAfter.length) {
745
653
  demoLog(
@@ -769,6 +677,7 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
769
677
  demoLog(" after dock-dom cleared (expected)");
770
678
  }
771
679
  }
680
+ if (process.env.WAYGRAPH_JSON !== "1") logBannerDom("after", await probeBanner(page));
772
681
  await gate();
773
682
  }
774
683
  await teardownOverlay(page);