waygraph 0.15.51 → 0.15.53

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 (153) hide show
  1. package/README.md +8 -1
  2. package/dist/analysis/graph.d.ts +1 -1
  3. package/dist/analysis/graph.js +10 -15
  4. package/dist/analysis/map-check.d.ts +3 -2
  5. package/dist/analysis/practices-check.d.ts +0 -1
  6. package/dist/analysis/practices-check.js +0 -13
  7. package/dist/auto-session/session.d.ts +1 -1
  8. package/dist/auto-session/session.js +13 -8
  9. package/dist/blocks-select.d.ts +7 -4
  10. package/dist/blocks-select.js +0 -14
  11. package/dist/browser.d.ts +27 -0
  12. package/dist/browser.js +62 -0
  13. package/dist/cli/flags.js +3 -1
  14. package/dist/cli/run-chain.js +4 -0
  15. package/dist/cli/usage.js +17 -1
  16. package/dist/cli.js +13 -1
  17. package/dist/commands/agent-dive-scaffold.d.ts +5 -20
  18. package/dist/commands/agent-dive-scaffold.js +10 -3
  19. package/dist/commands/extensions.d.ts +1 -0
  20. package/dist/commands/extensions.js +22 -0
  21. package/dist/commands/init.js +43 -5
  22. package/dist/commands/test.js +16 -2
  23. package/dist/commands/try.js +2 -1
  24. package/dist/commands/upgrade.d.ts +6 -0
  25. package/dist/commands/upgrade.js +113 -0
  26. package/dist/config.d.ts +81 -3
  27. package/dist/config.js +91 -16
  28. package/dist/core/block.d.ts +19 -2
  29. package/dist/core/block.js +43 -3
  30. package/dist/core/blocks/nav.d.ts +11 -0
  31. package/dist/core/blocks/nav.js +8 -0
  32. package/dist/core/config.d.ts +5 -0
  33. package/dist/core/engine.d.ts +19 -16
  34. package/dist/core/engine.js +6 -4
  35. package/dist/core/flow.d.ts +27 -0
  36. package/dist/core/flow.js +75 -28
  37. package/dist/core/map-builder.d.ts +30 -4
  38. package/dist/core/map-builder.js +45 -7
  39. package/dist/env-file.d.ts +7 -7
  40. package/dist/env-file.js +29 -16
  41. package/dist/extensions.d.ts +21 -0
  42. package/dist/extensions.js +178 -0
  43. package/dist/highlights/demo-pace.d.ts +0 -1
  44. package/dist/highlights/demo-pace.js +24 -3
  45. package/dist/index.d.ts +7 -5
  46. package/dist/index.js +3 -2
  47. package/dist/pilot-overlay/action-ring.d.ts +0 -9
  48. package/dist/pilot-overlay/action-ring.js +1 -1
  49. package/dist/pilot-overlay/overlay-fixtures.d.ts +0 -7
  50. package/dist/pilot-overlay/overlay-fixtures.js +0 -45
  51. package/dist/pilot-overlay/panel.d.ts +2 -1
  52. package/dist/playwright-config.d.ts +4 -0
  53. package/dist/playwright-config.js +15 -0
  54. package/dist/playwright.config.d.ts +2 -0
  55. package/dist/playwright.config.js +6 -0
  56. package/dist/runner/demo-log.js +1 -7
  57. package/dist/runner/inpage/core.d.ts +1 -1
  58. package/dist/runner/inpage/core.js +8 -7
  59. package/dist/runner/interaction-patch.js +32 -1
  60. package/dist/runner/main.js +22 -11
  61. package/dist/runner/rings.js +0 -49
  62. package/dist/runner/step-mode.js +10 -4
  63. package/dist/session/auto-explore-run.js +9 -5
  64. package/dist/session/auto-explore.d.ts +2 -1
  65. package/dist/session/auto-explore.js +6 -4
  66. package/dist/session/auto-session-ipc.d.ts +1 -2
  67. package/dist/session/auto-session-ipc.js +1 -1
  68. package/dist/session/block-inject.d.ts +0 -13
  69. package/dist/session/block-inject.js +3 -3
  70. package/dist/session/pilot.d.ts +0 -2
  71. package/dist/session/pilot.js +1 -2
  72. package/dist/session/step-overlay.js +3 -1
  73. package/dist/traverse/coverage.d.ts +0 -4
  74. package/dist/traverse/coverage.js +1 -1
  75. package/dist/traverse/run.js +3 -1
  76. package/dist/types.d.ts +1 -1
  77. package/dist/ui/components/ring.js +1 -0
  78. package/dist/ui/overlay.css +1 -0
  79. package/dist/ui/shadow.d.ts +1 -3
  80. package/dist/ui/shadow.js +0 -4
  81. package/dist/ui/tokens.d.ts +0 -11
  82. package/dist/ui/tokens.js +0 -5
  83. package/dist/video-dir.d.ts +8 -0
  84. package/dist/video-dir.js +60 -0
  85. package/dist/webserver.js +5 -1
  86. package/package.json +11 -5
  87. package/templates/scaffold/README.md +5 -3
  88. package/templates/scaffold/STRUCTURE.md +10 -4
  89. package/templates/scaffold/config/waygraph.config.ts +45 -0
  90. package/templates/scaffold/package.json +7 -7
  91. package/templates/scaffold/src/flows/(external)/hoppscotch/auth.flow.ts +36 -0
  92. package/templates/scaffold/src/flows/(external)/hoppscotch/get.flow.ts +24 -0
  93. package/templates/scaffold/src/flows/(external)/hoppscotch/post.flow.ts +34 -0
  94. package/templates/scaffold/src/flows/(user-stories)/email-verification.flow.ts +47 -0
  95. package/templates/scaffold/src/flows/README.md +35 -14
  96. package/templates/scaffold/src/flows/home/cart.flow.ts +51 -0
  97. package/templates/scaffold/src/flows/home/hello.flow.ts +42 -0
  98. package/templates/scaffold/src/map/(app_base)/docs/_nav.block.ts +0 -9
  99. package/templates/scaffold/src/map/(app_base)/docs/_page.block.ts +0 -5
  100. package/templates/scaffold/src/map/(app_base)/docs/_sel.ts +1 -1
  101. package/templates/scaffold/src/map/(app_base)/home/_methods/add-item.effect.block.ts +3 -19
  102. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-hello.method.block.ts +2 -25
  103. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-item-added.method.block.ts +2 -19
  104. package/templates/scaffold/src/map/(app_base)/home/_methods/clear-cart.method.block.ts +1 -17
  105. package/templates/scaffold/src/map/(app_base)/home/_methods/remove-item.effect.block.ts +1 -10
  106. package/templates/scaffold/src/map/(app_base)/home/_nav.block.ts +1 -12
  107. package/templates/scaffold/src/map/(app_base)/home/_page.block.ts +0 -9
  108. package/templates/scaffold/src/map/(app_base)/home/_sel.ts +3 -1
  109. package/templates/scaffold/src/map/(app_base)/home/nav-verification-link.block.ts +1 -23
  110. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/assert-response-body.method.block.ts +21 -0
  111. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/assert-response-header.method.block.ts +19 -0
  112. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/assert-status.method.block.ts +20 -0
  113. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/pick-extension-interceptor.method.block.ts +18 -0
  114. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/read-response-body.method.block.ts +21 -0
  115. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/read-response-headers.method.block.ts +21 -0
  116. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/send-request.method.block.ts +15 -0
  117. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-bearer-token.method.block.ts +20 -0
  118. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-headers.method.block.ts +23 -0
  119. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-json-body.method.block.ts +36 -0
  120. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-method.method.block.ts +17 -0
  121. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-query-params.method.block.ts +22 -0
  122. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-url.method.block.ts +18 -0
  123. package/templates/scaffold/src/map/(external)/hoppscotch/_nav.block.ts +11 -0
  124. package/templates/scaffold/src/map/(external)/hoppscotch/_page.block.ts +36 -0
  125. package/templates/scaffold/src/map/(external)/hoppscotch/_sel.ts +27 -0
  126. package/templates/scaffold/src/map/(external)/hoppscotch/settings/_nav.block.ts +10 -0
  127. package/templates/scaffold/src/map/(external)/mailpit/_methods/assert-email-content.method.block.ts +2 -27
  128. package/templates/scaffold/src/map/(external)/mailpit/_methods/assert-email-received.method.block.ts +2 -21
  129. package/templates/scaffold/src/map/(external)/mailpit/_methods/extract-email-link.method.block.ts +1 -19
  130. package/templates/scaffold/src/map/(external)/mailpit/_methods/open-message.method.block.ts +1 -11
  131. package/templates/scaffold/src/map/(external)/mailpit/_nav.block.ts +1 -9
  132. package/templates/scaffold/src/map/(external)/mailpit/_page.block.ts +0 -11
  133. package/templates/scaffold/src/states/demo.mem-keys.ts +20 -19
  134. package/templates/scaffold/src/states/demo.mem-stubs.ts +4 -0
  135. package/templates/scaffold/src/states/demo.states.ts +5 -1
  136. package/templates/scaffold/tests/example.spec.ts +6 -7
  137. package/templates/scaffold/tests/hoppscotch.spec.ts +97 -0
  138. package/templates/scaffold/tests/mail-verify.spec.ts +8 -17
  139. package/templates/scaffold/tsconfig.json +1 -1
  140. package/templates/skills/waygraph-convention.skill.md +40 -3
  141. package/templates/skills/waygraph-pilot.skill.md +4 -5
  142. package/dist/browser-path.d.ts +0 -5
  143. package/dist/browser-path.js +0 -18
  144. package/dist/runner/overlay-present.d.ts +0 -8
  145. package/dist/runner/overlay-present.js +0 -57
  146. package/dist/ui/components/banner.d.ts +0 -2
  147. package/dist/ui/components/banner.js +0 -10
  148. package/dist/ui/components/cursor.d.ts +0 -2
  149. package/dist/ui/components/cursor.js +0 -19
  150. package/templates/scaffold/playwright.config.ts +0 -12
  151. package/templates/scaffold/src/flows/example.flow.ts +0 -29
  152. package/templates/scaffold/src/flows/mail-verify.flow.ts +0 -85
  153. package/templates/scaffold/src/flows/shop.flow.ts +0 -74
package/dist/env-file.js CHANGED
@@ -28,31 +28,44 @@ function parseEnvFile(content) {
28
28
  }
29
29
  return out;
30
30
  }
31
+ /** The env mode: `--env <mode>` / `WAYGRAPH_ENV`, else `NODE_ENV`, else "development". */
32
+ export function envMode() {
33
+ return process.env.WAYGRAPH_ENV || process.env.NODE_ENV || "development";
34
+ }
35
+ /** Removes `--env <mode>` / `--env=<mode>` from argv and records it as `WAYGRAPH_ENV`, so it never reaches a forwarded command. */
36
+ export function takeEnvFlag(argv) {
37
+ const out = [];
38
+ for (let i = 0; i < argv.length; i++) {
39
+ const a = argv[i];
40
+ if (a === "--env" && argv[i + 1] !== undefined)
41
+ process.env.WAYGRAPH_ENV = argv[++i];
42
+ else if (a.startsWith("--env="))
43
+ process.env.WAYGRAPH_ENV = a.slice("--env=".length);
44
+ else
45
+ out.push(a);
46
+ }
47
+ return out;
48
+ }
31
49
  /**
32
- * Auto-loads `.env` / `.env.local` / `.env.<mode>` / `.env.<mode>.local` from a project's root,
33
- * same layering Vite/Next use (`mode` defaults to `NODE_ENV`, falling back to "development"),
34
- * merged into `process.env` in increasing priority. A real, already-set `process.env` value -
35
- * whatever the shell actually exported - always wins over every file; a `.env` file only fills
36
- * a gap, it never silently overrides something the caller explicitly set. Safe to call more
37
- * than once (e.g. once eagerly at `cwd`, again once the real project dir is known) - loading
38
- * twice is a no-op the second time for any key already filled.
50
+ * Loads `.env` / `.env.local` / `.env.<mode>` / `.env.<mode>.local` from a project's root, the layering
51
+ * Vite and Next use: each later file overrides the earlier ones (`.env.<mode>.local` wins). A value the
52
+ * shell already exported always beats every file, so a file only fills gaps. Calling it twice is harmless.
39
53
  */
40
- export function loadEnvFiles(projectDir, mode = process.env.NODE_ENV || "development") {
41
- const files = [".env", ".env.local", `.env.${mode}`, `.env.${mode}.local`];
42
- for (const name of files) {
54
+ export function loadEnvFiles(projectDir, mode = envMode()) {
55
+ const merged = {};
56
+ for (const name of [".env", ".env.local", `.env.${mode}`, `.env.${mode}.local`]) {
43
57
  const p = join(projectDir, name);
44
58
  if (!existsSync(p))
45
59
  continue;
46
- let parsed;
47
60
  try {
48
- parsed = parseEnvFile(readFileSync(p, "utf-8"));
61
+ Object.assign(merged, parseEnvFile(readFileSync(p, "utf-8")));
49
62
  }
50
63
  catch {
51
64
  continue; // unreadable file - never fail a run over an optional .env
52
65
  }
53
- for (const [key, value] of Object.entries(parsed)) {
54
- if (process.env[key] === undefined)
55
- process.env[key] = value;
56
- }
66
+ }
67
+ for (const [key, value] of Object.entries(merged)) {
68
+ if (process.env[key] === undefined)
69
+ process.env[key] = value;
57
70
  }
58
71
  }
@@ -0,0 +1,21 @@
1
+ /** A known name (`"hoppscotch"`), a Chrome Web Store id, a path to an unpacked extension, or `{ id, sha256 }`. */
2
+ export type ExtensionSpec = string | {
3
+ id?: string;
4
+ path?: string;
5
+ sha256?: string;
6
+ };
7
+ export declare const KNOWN_EXTENSIONS: Readonly<Record<string, string>>;
8
+ /** Unpacks a .crx and pins the manifest `key`, so the extension keeps its store id (its origin is then stable, e.g. for an API allowlist). */
9
+ export declare function unpackCrx(crx: Buffer, dest: string): void;
10
+ /**
11
+ * Returns an unpacked directory for one config entry, downloading it on first use. An installed
12
+ * extension is never silently replaced - pass `update: true` (`waygraph extensions install --update`) to refetch.
13
+ */
14
+ export declare function resolveExtension(spec: ExtensionSpec, projectDir: string, opts?: {
15
+ update?: boolean;
16
+ }): Promise<string>;
17
+ export declare function resolveExtensions(specs: readonly ExtensionSpec[] | undefined, projectDir: string): Promise<string[]>;
18
+ export declare function extensionInfo(dir: string): {
19
+ name: string;
20
+ version: string;
21
+ };
@@ -0,0 +1,178 @@
1
+ // Browser extensions for waygraph runs: resolve a config entry to an unpacked directory Chromium can load.
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join, resolve, sep } from "node:path";
6
+ import { inflateRawSync } from "node:zlib";
7
+ export const KNOWN_EXTENSIONS = {
8
+ hoppscotch: "amknoiejhlmhancpahfcfcfhllgkpbld",
9
+ };
10
+ const STORE_ID = /^[a-p]{32}$/;
11
+ function cacheDir() {
12
+ const base = process.env.WAYGRAPH_CACHE_DIR ?? join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "waygraph");
13
+ return join(base, "extensions");
14
+ }
15
+ /** Reads the end-of-central-directory record and extracts every entry; rejects paths that escape `dest`. */
16
+ function unzipTo(zip, dest) {
17
+ let eocd = zip.length - 22;
18
+ while (eocd >= 0 && zip.readUInt32LE(eocd) !== 0x06054b50)
19
+ eocd--;
20
+ if (eocd < 0)
21
+ throw new Error("not a zip archive");
22
+ const count = zip.readUInt16LE(eocd + 10);
23
+ let p = zip.readUInt32LE(eocd + 16);
24
+ const root = resolve(dest);
25
+ for (let i = 0; i < count; i++) {
26
+ if (zip.readUInt32LE(p) !== 0x02014b50)
27
+ throw new Error("corrupt zip directory");
28
+ const method = zip.readUInt16LE(p + 10);
29
+ const size = zip.readUInt32LE(p + 20);
30
+ const nameLen = zip.readUInt16LE(p + 28);
31
+ const extraLen = zip.readUInt16LE(p + 30);
32
+ const commentLen = zip.readUInt16LE(p + 32);
33
+ const local = zip.readUInt32LE(p + 42);
34
+ const name = zip.toString("utf8", p + 46, p + 46 + nameLen);
35
+ p += 46 + nameLen + extraLen + commentLen;
36
+ const target = resolve(root, name);
37
+ if (target !== root && !target.startsWith(root + sep))
38
+ throw new Error(`unsafe path in archive: ${name}`);
39
+ if (name.endsWith("/")) {
40
+ mkdirSync(target, { recursive: true });
41
+ continue;
42
+ }
43
+ const dataStart = local + 30 + zip.readUInt16LE(local + 26) + zip.readUInt16LE(local + 28);
44
+ const raw = zip.subarray(dataStart, dataStart + size);
45
+ if (method !== 0 && method !== 8)
46
+ throw new Error(`unsupported zip compression (${method}) for ${name}`);
47
+ mkdirSync(dirname(target), { recursive: true });
48
+ writeFileSync(target, method === 0 ? raw : inflateRawSync(raw));
49
+ }
50
+ }
51
+ /** A .crx is a signed header followed by a normal zip. The signature is not checked here - pin `sha256` to do that. */
52
+ function crxToZip(crx) {
53
+ if (crx.toString("latin1", 0, 4) !== "Cr24")
54
+ throw new Error("not a Chrome extension (.crx) file");
55
+ const version = crx.readUInt32LE(4);
56
+ if (version === 3)
57
+ return crx.subarray(12 + crx.readUInt32LE(8));
58
+ if (version === 2)
59
+ return crx.subarray(16 + crx.readUInt32LE(8) + crx.readUInt32LE(12));
60
+ throw new Error(`unsupported .crx version ${version}`);
61
+ }
62
+ /** Reads the length-delimited fields of one protobuf message as [fieldNumber, bytes] pairs; other wire types are skipped. */
63
+ function protoFields(buf) {
64
+ const out = [];
65
+ let p = 0;
66
+ const varint = () => {
67
+ let n = 0;
68
+ for (let shift = 0;; shift += 7) {
69
+ const b = buf[p++];
70
+ n += (b & 0x7f) * 2 ** shift;
71
+ if (!(b & 0x80))
72
+ return n;
73
+ }
74
+ };
75
+ while (p < buf.length) {
76
+ const tag = varint();
77
+ const wire = tag & 7;
78
+ if (wire === 2) {
79
+ const len = varint();
80
+ out.push([tag >>> 3, buf.subarray(p, p + len)]);
81
+ p += len;
82
+ }
83
+ else if (wire === 0)
84
+ varint();
85
+ else if (wire === 1)
86
+ p += 8;
87
+ else if (wire === 5)
88
+ p += 4;
89
+ else
90
+ break;
91
+ }
92
+ return out;
93
+ }
94
+ /** The extension's public key (DER), from which Chrome derives its id: with it in the manifest the id equals the store's. */
95
+ function crxPublicKey(crx) {
96
+ const version = crx.readUInt32LE(4);
97
+ if (version === 2)
98
+ return crx.subarray(16, 16 + crx.readUInt32LE(8));
99
+ const header = protoFields(crx.subarray(12, 12 + crx.readUInt32LE(8)));
100
+ const signed = header.find(([n]) => n === 10000)?.[1];
101
+ const crxId = signed ? protoFields(signed).find(([n]) => n === 1)?.[1] : undefined;
102
+ for (const [n, proof] of header) {
103
+ if (n !== 2 && n !== 3)
104
+ continue;
105
+ const key = protoFields(proof).find(([f]) => f === 1)?.[1];
106
+ if (key && crxId && createHash("sha256").update(key).digest().subarray(0, 16).equals(crxId))
107
+ return key;
108
+ }
109
+ return undefined;
110
+ }
111
+ /** Unpacks a .crx and pins the manifest `key`, so the extension keeps its store id (its origin is then stable, e.g. for an API allowlist). */
112
+ export function unpackCrx(crx, dest) {
113
+ unzipTo(crxToZip(crx), dest);
114
+ const key = crxPublicKey(crx);
115
+ if (!key)
116
+ return;
117
+ const file = join(dest, "manifest.json");
118
+ const manifest = JSON.parse(readFileSync(file, "utf-8"));
119
+ manifest.key = key.toString("base64");
120
+ writeFileSync(file, JSON.stringify(manifest, null, 2));
121
+ }
122
+ function readManifest(dir) {
123
+ const file = join(dir, "manifest.json");
124
+ if (!existsSync(file))
125
+ throw new Error(`no manifest.json in ${dir}`);
126
+ return JSON.parse(readFileSync(file, "utf-8"));
127
+ }
128
+ async function download(id, sha256) {
129
+ const url = "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=130.0.0.0" +
130
+ `&acceptformat=crx2,crx3&x=id%3D${id}%26uc`;
131
+ const res = await fetch(url);
132
+ const body = Buffer.from(await res.arrayBuffer());
133
+ if (!res.ok || body.length < 16 || body.toString("latin1", 0, 4) !== "Cr24") {
134
+ throw new Error(`extension ${id} was not found in the Chrome Web Store (HTTP ${res.status})`);
135
+ }
136
+ if (sha256 && createHash("sha256").update(body).digest("hex") !== sha256.toLowerCase()) {
137
+ throw new Error(`extension ${id}: downloaded file does not match the pinned sha256`);
138
+ }
139
+ return body;
140
+ }
141
+ /**
142
+ * Returns an unpacked directory for one config entry, downloading it on first use. An installed
143
+ * extension is never silently replaced - pass `update: true` (`waygraph extensions install --update`) to refetch.
144
+ */
145
+ export async function resolveExtension(spec, projectDir, opts = {}) {
146
+ const entry = typeof spec === "string" ? (spec.startsWith(".") || spec.includes("/") ? { path: spec } : { id: spec }) : spec;
147
+ if (entry.path) {
148
+ const dir = resolve(projectDir, entry.path);
149
+ readManifest(dir);
150
+ return dir;
151
+ }
152
+ const id = KNOWN_EXTENSIONS[entry.id ?? ""] ?? entry.id ?? "";
153
+ if (!STORE_ID.test(id)) {
154
+ throw new Error(`unknown extension "${entry.id}" - use a known name (${Object.keys(KNOWN_EXTENSIONS).join(", ")}), a Chrome Web Store id, or { path }`);
155
+ }
156
+ const dir = join(cacheDir(), id);
157
+ if (!opts.update && existsSync(join(dir, "manifest.json")))
158
+ return dir;
159
+ const tmp = `${dir}.tmp-${process.pid}`;
160
+ rmSync(tmp, { recursive: true, force: true });
161
+ try {
162
+ unpackCrx(await download(id, entry.sha256), tmp);
163
+ readManifest(tmp);
164
+ rmSync(dir, { recursive: true, force: true });
165
+ renameSync(tmp, dir);
166
+ }
167
+ finally {
168
+ rmSync(tmp, { recursive: true, force: true });
169
+ }
170
+ return dir;
171
+ }
172
+ export async function resolveExtensions(specs, projectDir) {
173
+ return Promise.all((specs ?? []).map((s) => resolveExtension(s, projectDir)));
174
+ }
175
+ export function extensionInfo(dir) {
176
+ const m = readManifest(dir);
177
+ return { name: m.name ?? dir, version: m.version ?? "?" };
178
+ }
@@ -10,7 +10,6 @@ export declare const SLIDE_DURATION_MS_DEFAULT = 2000;
10
10
  /** @deprecated Prefer FIXTURE_DURATION_FAST_MS_DEFAULT */
11
11
  export declare const SLIDE_DURATION_FAST_MS_DEFAULT = 600;
12
12
  export declare function normalizeDemoPace(raw: unknown): DemoPace;
13
- /** Named / numeric pace -> scale vs normal (1 = normal). Absolute ms (>20) maps via default dwell. */
14
13
  export declare function demoPaceScale(pace: DemoPace): number;
15
14
  export declare function demoPaceIsBlitz(pace: DemoPace): boolean;
16
15
  /** True when pace should use the short-gate / fast fixture path (not slow authored). */
@@ -34,6 +34,24 @@ export function normalizeDemoPace(raw) {
34
34
  return "normal";
35
35
  }
36
36
  /** Named / numeric pace -> scale vs normal (1 = normal). Absolute ms (>20) maps via default dwell. */
37
+ /**
38
+ * What each named pace means, as a multiplier of normal dwell (1 = normal). `demo.speeds` in the config (or
39
+ * `WAYGRAPH_PACE_SPEEDS`, a JSON object) overrides any of them; the defaults are blitz 0.05, fast 0.33, slow 2.
40
+ */
41
+ function paceSpeeds() {
42
+ const out = { blitz: 0.05, fast: 0.33, slow: 2 };
43
+ try {
44
+ const raw = process.env.WAYGRAPH_PACE_SPEEDS ? JSON.parse(process.env.WAYGRAPH_PACE_SPEEDS) : {};
45
+ for (const k of ["blitz", "fast", "slow"]) {
46
+ if (typeof raw[k] === "number" && raw[k] > 0)
47
+ out[k] = Math.min(10, raw[k]);
48
+ }
49
+ }
50
+ catch {
51
+ /* ignore a malformed value: defaults apply */
52
+ }
53
+ return out;
54
+ }
37
55
  export function demoPaceScale(pace) {
38
56
  if (typeof pace === "number") {
39
57
  if (pace > 20)
@@ -41,11 +59,11 @@ export function demoPaceScale(pace) {
41
59
  return Math.min(10, Math.max(0.05, pace));
42
60
  }
43
61
  if (pace === "blitz")
44
- return 0.05;
62
+ return paceSpeeds().blitz;
45
63
  if (pace === "fast")
46
- return 0.33;
64
+ return paceSpeeds().fast;
47
65
  if (pace === "slow")
48
- return 2;
66
+ return paceSpeeds().slow;
49
67
  return 1;
50
68
  }
51
69
  export function demoPaceIsBlitz(pace) {
@@ -73,6 +91,9 @@ export function demoPaceIsSlow(pace) {
73
91
  }
74
92
  /** Autoplay / Next-gate ms for this pace (base is WAYGRAPH_AUTOPLAY_MS or 1800). */
75
93
  export function demoPaceGateMs(pace, baseAutoplayMs) {
94
+ if (pace === "blitz" && process.env.WAYGRAPH_PACE_SPEEDS) {
95
+ return Math.min(30000, Math.max(20, Math.round(baseAutoplayMs * paceSpeeds().blitz)));
96
+ }
76
97
  if (demoPaceIsBlitz(pace))
77
98
  return Math.min(80, baseAutoplayMs);
78
99
  if (typeof pace === "number" && pace > 20) {
package/dist/index.d.ts CHANGED
@@ -6,13 +6,13 @@ export { css, byText, byRole } from "./selector.js";
6
6
  export { WaygraphError, WAYGRAPH_ERRORS } from "./errors.js";
7
7
  export type { WaygraphErrorCode } from "./errors.js";
8
8
  export { Trait, urlMatches, textEquals, visible, disabled, enabled, frameVisible, frameTextEquals, frameContainsText, } from "./trait.js";
9
- export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, defineApiBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withMemStub, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, chainFlow, defineLayout, map, MapBuilder, branchRoutes, collectBranchFlows, runBranchRegression, } from "./core.js";
9
+ export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, defineApiBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withMemStub, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, withDialog, chainFlow, defineLayout, map, MapBuilder, branchRoutes, collectBranchFlows, runBranchRegression, } from "./core.js";
10
10
  export type { BranchEntry, BranchRunResult, RunBranchRegressionOptions } from "./core/branch-regression.js";
11
11
  export type { ApiBlockOptions, ApiCallResult } from "./core/blocks/api.js";
12
- export type { MapStepOpts } from "./core/map-builder.js";
12
+ export type { MapStepOpts, MapFlowOpts } from "./core/map-builder.js";
13
13
  export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, DEVICE_PRESETS, resolveTodoDockUi, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, WAYGRAPH_RING_CSS, } from "./highlights.js";
14
14
  export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, WaygraphTodoItem, WaygraphTodoInput, WaygraphTodoGroup, WaygraphTodoGroupInput, WaygraphTodosOpts, TodoDockUiOpts, BannerPos, BannerUiOpts, TodoDockUiResolved, TodoDockState, TodoListStyle, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, StubCtx, StubLifecycleFn, StubPhaseResult, StubPhaseFixtures, DevicePreset, DeviceViewport, DeviceState, DeviceOrientation, } from "./highlights.js";
15
- export type { Flow, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, AssertBlock, AssertBlockOptions, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, Layout, LayoutOptions, MapBuilderOptions, } from "./core.js";
15
+ export type { Flow, FlowOptions, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, AssertBlock, AssertBlockOptions, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, Layout, LayoutOptions, MapBuilderOptions, } from "./core.js";
16
16
  export type { ActionPage } from "./types.js";
17
17
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./analysis/graph.js";
18
18
  export type { WaygraphGraph, WaygraphNode, WaygraphEdge, SkippedBlock, OrphanBlock } from "./analysis/graph.js";
@@ -30,7 +30,9 @@ export type { AutoSessionInit, SessionSnapshot, SessionSnapshotEdge, SessionSnap
30
30
  export { pilotStart } from "./session/pilot.js";
31
31
  export type { PilotStartResult } from "./session/pilot.js";
32
32
  export { defineConfig, resolveBaseUrl } from "./config.js";
33
- export type { WaygraphConfig } from "./config.js";
33
+ export type { WaygraphConfig, ConfigContext, VideoSettings, RunSettings, DemoSettings } from "./config.js";
34
34
  export { withWebServer } from "./webserver.js";
35
- export { chromiumExecutablePath } from "./browser-path.js";
35
+ export { chromiumExecutablePath } from "./browser.js";
36
+ export { resolveExtension, KNOWN_EXTENSIONS } from "./extensions.js";
37
+ export type { ExtensionSpec } from "./extensions.js";
36
38
  export type { WebServerConfig } from "./webserver.js";
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ export { registerMemStub, seedMemStub, getMemStub } from "./mem-stub.js";
4
4
  export { css, byText, byRole } from "./selector.js";
5
5
  export { WaygraphError, WAYGRAPH_ERRORS } from "./errors.js";
6
6
  export { Trait, urlMatches, textEquals, visible, disabled, enabled, frameVisible, frameTextEquals, frameContainsText, } from "./trait.js";
7
- export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, defineApiBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withMemStub, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, chainFlow, defineLayout, map, MapBuilder, branchRoutes, collectBranchFlows, runBranchRegression, } from "./core.js";
7
+ export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, defineApiBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withMemStub, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, withDialog, chainFlow, defineLayout, map, MapBuilder, branchRoutes, collectBranchFlows, runBranchRegression, } from "./core.js";
8
8
  export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, DEVICE_PRESETS, resolveTodoDockUi, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, WAYGRAPH_RING_CSS, } from "./highlights.js";
9
9
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./analysis/graph.js";
10
10
  export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp, } from "./blocks-select.js";
@@ -16,4 +16,5 @@ export { AutoSession, stubFilePath } from "./auto-session.js";
16
16
  export { pilotStart } from "./session/pilot.js";
17
17
  export { defineConfig, resolveBaseUrl } from "./config.js";
18
18
  export { withWebServer } from "./webserver.js";
19
- export { chromiumExecutablePath } from "./browser-path.js";
19
+ export { chromiumExecutablePath } from "./browser.js";
20
+ export { resolveExtension, KNOWN_EXTENSIONS } from "./extensions.js";
@@ -1,16 +1,7 @@
1
1
  import type { Page } from "@playwright/test";
2
- type Box = {
3
- x: number;
4
- y: number;
5
- width: number;
6
- height: number;
7
- };
8
- /** Paint the gray "auto" ring (Playwright is about to act here) at a viewport rectangle. */
9
- export declare function showPilotRect(page: Page, box: Box, label: string, tone?: string): Promise<void>;
10
2
  /**
11
3
  * Every Playwright instruction a Block runs against this session's page gets a gray (auto) ring + robot
12
4
  * label first, so a human watching a pilot window sees WHAT is being driven. `dwellMs` holds the ring
13
5
  * long enough to read before the action fires (0 for headless).
14
6
  */
15
7
  export declare function instrumentPilotActions(page: Page, dwellMs: number): void;
16
- export {};
@@ -1,6 +1,6 @@
1
1
  import { ensureInstalled } from "./shell.js";
2
2
  /** Paint the gray "auto" ring (Playwright is about to act here) at a viewport rectangle. */
3
- export async function showPilotRect(page, box, label, tone = "auto") {
3
+ async function showPilotRect(page, box, label, tone = "auto") {
4
4
  await ensureInstalled(page);
5
5
  await page
6
6
  .evaluate(({ box, label, tone }) => {
@@ -1,12 +1,5 @@
1
1
  import type { StubPhaseResult } from "../highlights.js";
2
2
  import type { PilotHighlightFixtures } from "./fixtures.js";
3
- /**
4
- * Resolve agent-sent "waygraph overlay fixtures" (`auto highlight` JSON / shorthand) through the SAME
5
- * stub-ctx path a Block's `stubBefore(ctx)` uses, so ctx.todos / ctx.device / ctx.title / ctx.bannerUi semantics
6
- * (keep vs set vs clear, defaults, normalisation) cannot diverge between `waygraph demo` and pilot/auto/browser.
7
- * Not Playwright test fixtures - these describe what the waygraph overlay paints on the page under test.
8
- */
9
- export declare function overlayFixturesToPhase(f: PilotHighlightFixtures): Promise<StubPhaseResult>;
10
3
  /**
11
4
  * The reverse: a resolved stub phase (what `runStubPhase` returns for a Block's stubBefore/After/OnError)
12
5
  * -> what the Pilot painter takes. Lets `auto/browser/pilot send` paint a Block's authored waygraph overlay
@@ -1,48 +1,3 @@
1
- import { runStubPhase } from "../highlights.js";
2
- /**
3
- * Resolve agent-sent "waygraph overlay fixtures" (`auto highlight` JSON / shorthand) through the SAME
4
- * stub-ctx path a Block's `stubBefore(ctx)` uses, so ctx.todos / ctx.device / ctx.title / ctx.bannerUi semantics
5
- * (keep vs set vs clear, defaults, normalisation) cannot diverge between `waygraph demo` and pilot/auto/browser.
6
- * Not Playwright test fixtures - these describe what the waygraph overlay paints on the page under test.
7
- */
8
- export async function overlayFixturesToPhase(f) {
9
- const lifecycle = (ctx) => {
10
- if (f.clear) {
11
- ctx.hideTodos();
12
- ctx.hideDevice();
13
- return;
14
- }
15
- if (f.title !== undefined)
16
- ctx.title(f.title, f.titlePos ? { pos: f.titlePos } : undefined);
17
- else if (f.titlePos)
18
- ctx.titlePos(f.titlePos);
19
- if (f.bannerUi)
20
- ctx.bannerUi(f.bannerUi);
21
- if (f.todos !== undefined) {
22
- ctx.todos("pilot", f.todos, {
23
- title: f.todoTitle ?? "Plan",
24
- index: f.todoIndex ?? 0,
25
- pos: f.todoPos === "left" ? "left" : "right",
26
- });
27
- }
28
- const ui = f.todoDockUi ?? f.todoUi;
29
- if (ui)
30
- ctx.todoDockUi(ui);
31
- if (f.device)
32
- ctx.device(f.device);
33
- if (typeof f.zoom === "number" && Number.isFinite(f.zoom) && f.zoom > 0)
34
- ctx.zoom(f.zoom);
35
- if (f.zoomOut !== undefined)
36
- ctx.zoomOut(f.zoomOut);
37
- if (f.rings?.length) {
38
- f.rings.forEach((r, i) => {
39
- const { selector, label, ...rest } = r;
40
- ctx.ring(`ring${i + 1}`, { selector, label, ...rest });
41
- });
42
- }
43
- };
44
- return runStubPhase({ name: "overlay-fixtures", stubBefore: lifecycle }, "stubBefore");
45
- }
46
1
  /**
47
2
  * The reverse: a resolved stub phase (what `runStubPhase` returns for a Block's stubBefore/After/OnError)
48
3
  * -> what the Pilot painter takes. Lets `auto/browser/pilot send` paint a Block's authored waygraph overlay
@@ -1,7 +1,7 @@
1
1
  import type { SessionSnapshot } from "../auto-session.js";
2
2
  import type { Page } from "@playwright/test";
3
3
  /** Minimal shape needed to render the graph tree - matches WaygraphGraph. */
4
- export interface PilotOverlayGraph {
4
+ interface PilotOverlayGraph {
5
5
  nodes: readonly {
6
6
  checkpoint: string;
7
7
  }[];
@@ -32,3 +32,4 @@ export interface PilotOverlayInfo {
32
32
  * operation over.
33
33
  */
34
34
  export declare function updatePilotOverlay(page: Page, info: PilotOverlayInfo): Promise<void>;
35
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { PlaywrightTestConfig } from "@playwright/test";
2
+ import type { WaygraphConfig } from "./config.js";
3
+ /** The test runner's config for a project, built from its waygraph config. `waygraph test` uses this when there is no playwright.config. */
4
+ export declare function playwrightConfigFrom(config: WaygraphConfig, projectDir: string, baseURL: string | undefined): PlaywrightTestConfig;
@@ -0,0 +1,15 @@
1
+ import { join } from "node:path";
2
+ /** The test runner's config for a project, built from its waygraph config. `waygraph test` uses this when there is no playwright.config. */
3
+ export function playwrightConfigFrom(config, projectDir, baseURL) {
4
+ const { use, ...rest } = config.playwright ?? {};
5
+ const webServers = [config.webServer ?? []].flat().map((s) => ({ ...s, cwd: join(projectDir, s.cwd ?? ".") }));
6
+ return {
7
+ testDir: join(projectDir, config.testDir ?? "tests"),
8
+ outputDir: join(projectDir, "test-results"),
9
+ fullyParallel: true,
10
+ reporter: [["list"], ["html", { open: "never", outputFolder: join(projectDir, "playwright-report") }]],
11
+ ...(webServers.length > 0 ? { webServer: webServers } : {}),
12
+ ...rest,
13
+ use: { baseURL, trace: "retain-on-failure", ...use },
14
+ };
15
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("@playwright/test").PlaywrightTestConfig<{}, {}>;
2
+ export default _default;
@@ -0,0 +1,6 @@
1
+ // Loaded by `waygraph test` (playwright test -c) for projects that keep their settings in config/waygraph.config.ts.
2
+ import { defineConfig } from "@playwright/test";
3
+ import { loadWaygraphConfig, resolveBaseUrl } from "./config.js";
4
+ import { playwrightConfigFrom } from "./playwright-config.js";
5
+ const projectDir = process.env.WAYGRAPH_PROJECT_DIR ?? process.cwd();
6
+ export default defineConfig(playwrightConfigFrom((await loadWaygraphConfig(projectDir)) ?? {}, projectDir, await resolveBaseUrl(projectDir)));
@@ -109,14 +109,8 @@ function demoTs() {
109
109
  );
110
110
  }
111
111
 
112
- let logSurface = "demo";
113
- /** Which surface is talking (demo | auto | browser | pilot) - same log format everywhere, only the tag differs. */
114
- export function setDemoLogSurface(name) {
115
- logSurface = String(name || "demo");
116
- }
117
-
118
112
  function demoPrefix() {
119
- return ansiPaint(ANSI.dim, "[" + demoTs() + "]") + " waygraph " + logSurface + ": ";
113
+ return ansiPaint(ANSI.dim, "[" + demoTs() + "]") + " waygraph demo: ";
120
114
  }
121
115
 
122
116
  export function demoLog(msg) {
@@ -1,4 +1,4 @@
1
1
  /** In-page installer (page.evaluate target) - see core.js's own header. Installs the shared ring
2
- * primitive (__wgEnsureRingEl/__wgPaintRingAt/__wgHideRingAt), cursor, click-pulse, focus veil,
2
+ * primitive (__wgEnsureRingEl/__wgPaintRingAt), cursor, click-pulse, focus veil,
3
3
  * and the #wg-ring singleton every surface's "current action" ring uses. */
4
4
  export function installCore(args: Record<string, unknown>): void;
@@ -72,7 +72,14 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
72
72
  ring.style.width = width + "px";
73
73
  ring.style.height = height + "px";
74
74
  ring.style.opacity = "1";
75
- ringLabel.textContent = label || "";
75
+ if (labelId === "wg-ring-label") {
76
+ // The demo ring's caption is drawn from data-label (CSS ::after), not DOM text, so the page's own
77
+ // text selectors (getByText, text=...) never match waygraph's overlay twice.
78
+ ringLabel.dataset.label = label || "";
79
+ ringLabel.textContent = "";
80
+ } else {
81
+ ringLabel.textContent = label || "";
82
+ }
76
83
  ringLabel.style.opacity = "1";
77
84
  const lw = ringLabel.offsetWidth;
78
85
  const lh = ringLabel.offsetHeight;
@@ -104,12 +111,6 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
104
111
  ringLabel.style.top = labelTop + "px";
105
112
  return [ring, ringLabel];
106
113
  };
107
- window.__wgHideRingAt = (ringId, labelId) => {
108
- const ring = __wgById(ringId);
109
- const label = __wgById(labelId);
110
- if (ring) ring.style.opacity = "0";
111
- if (label) label.style.opacity = "0";
112
- };
113
114
  window.__wgEnsureRingEl("wg-ring", "wg-ring-label");
114
115
  if (!__wgById("wg-cursor")) {
115
116
  const cursor = document.createElement("div");
@@ -21,7 +21,36 @@ export function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stu
21
21
  // (shorter Next/autoplay gates) and must NOT skip theater, or the whole demo
22
22
  // looks like plain waygraph run.
23
23
  const skipTheater = () => !!pacing.skipTheater;
24
- const typeDelay = () => (skipTheater() ? 0 : slowMo ? 0 : 30);
24
+ // `slowMo` here is the demo's own beat: ms to rest after each action (once per call, never per keystroke). The browser
25
+ // itself is launched with slowMo 0 in step mode, so a step whose pace is blitz really does run at full speed.
26
+ const beat = () => (skipTheater() ? 0 : slowMo || 0);
27
+ const pause = (ms) => (ms > 0 ? new Promise((res) => setTimeout(res, ms)) : undefined);
28
+ // Per-character typing delay (WAYGRAPH_TYPING_MS, default 30). 0 for blitz steps.
29
+ const typingMs = () => {
30
+ if (skipTheater()) return 0;
31
+ const n = Number(process.env.WAYGRAPH_TYPING_MS);
32
+ return process.env.WAYGRAPH_TYPING_MS !== undefined && Number.isFinite(n) && n >= 0 ? n : 30;
33
+ };
34
+ const typeDelay = typingMs;
35
+ const keyboard = page.keyboard;
36
+ if (!keyboard.__wgBeatPatched) {
37
+ keyboard.__wgBeatPatched = true;
38
+ const originalType = keyboard.type.bind(keyboard);
39
+ keyboard.type = async (text, options) => {
40
+ await originalType(text, { delay: typingMs(), ...(options || {}) });
41
+ await pause(beat());
42
+ };
43
+ const originalPress = keyboard.press.bind(keyboard);
44
+ keyboard.press = async (key, options) => {
45
+ await originalPress(key, options);
46
+ await pause(beat());
47
+ };
48
+ const originalInsertText = keyboard.insertText.bind(keyboard);
49
+ keyboard.insertText = async (text) => {
50
+ await originalInsertText(text);
51
+ await pause(beat());
52
+ };
53
+ }
25
54
  // Click is a single action, not per-character, so it doesn't compound
26
55
  // the same way - but slowMo still adds its own pause around the actual
27
56
  // click, so trim our own explicit "pop" pauses when it's already active
@@ -155,6 +184,7 @@ export function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stu
155
184
  // it - it was sticking around through the whole rest of the step.
156
185
  await hideRing(page);
157
186
  await restoreTheaterAfterNavigation(page, todoDockRef, deviceRef, stubBeforeRef);
187
+ await pause(beat());
158
188
  return result;
159
189
  };
160
190
  }
@@ -285,6 +315,7 @@ export function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stu
285
315
  await hideRing(page);
286
316
  // Full document navigation drops #wg-todo-dock + rings; restore carry.
287
317
  await restoreTheaterAfterNavigation(page, todoDockRef, deviceRef, stubBeforeRef);
318
+ await pause(beat());
288
319
  return result;
289
320
  };
290
321
  }