waygraph 0.15.52 → 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 (59) hide show
  1. package/README.md +8 -1
  2. package/dist/analysis/map-check.d.ts +3 -2
  3. package/dist/cli/flags.js +3 -1
  4. package/dist/cli/usage.js +4 -1
  5. package/dist/cli.js +4 -1
  6. package/dist/commands/agent-dive-scaffold.d.ts +0 -20
  7. package/dist/commands/agent-dive-scaffold.js +3 -3
  8. package/dist/commands/init.js +1 -1
  9. package/dist/commands/upgrade.d.ts +1 -1
  10. package/dist/commands/upgrade.js +7 -1
  11. package/dist/config.d.ts +53 -19
  12. package/dist/config.js +55 -13
  13. package/dist/core/block.d.ts +7 -2
  14. package/dist/core/block.js +7 -3
  15. package/dist/core/engine.d.ts +2 -0
  16. package/dist/core/engine.js +3 -1
  17. package/dist/core/flow.d.ts +10 -0
  18. package/dist/core/flow.js +32 -8
  19. package/dist/core/map-builder.d.ts +27 -1
  20. package/dist/core/map-builder.js +42 -4
  21. package/dist/highlights/demo-pace.d.ts +0 -1
  22. package/dist/highlights/demo-pace.js +24 -3
  23. package/dist/index.d.ts +2 -2
  24. package/dist/pilot-overlay/action-ring.d.ts +0 -9
  25. package/dist/pilot-overlay/action-ring.js +1 -1
  26. package/dist/pilot-overlay/panel.d.ts +2 -1
  27. package/dist/runner/interaction-patch.js +32 -1
  28. package/dist/runner/main.js +5 -2
  29. package/dist/runner/step-mode.js +10 -4
  30. package/dist/session/auto-explore.d.ts +2 -1
  31. package/dist/session/auto-session-ipc.d.ts +1 -2
  32. package/dist/session/auto-session-ipc.js +1 -1
  33. package/dist/session/block-inject.d.ts +0 -13
  34. package/dist/session/block-inject.js +3 -3
  35. package/dist/session/pilot.d.ts +0 -2
  36. package/dist/session/pilot.js +1 -2
  37. package/dist/traverse/coverage.d.ts +0 -4
  38. package/dist/traverse/coverage.js +1 -1
  39. package/dist/types.d.ts +1 -1
  40. package/package.json +11 -5
  41. package/templates/scaffold/README.md +1 -1
  42. package/templates/scaffold/STRUCTURE.md +5 -2
  43. package/templates/scaffold/config/waygraph.config.ts +24 -5
  44. package/templates/scaffold/package.json +6 -6
  45. package/templates/scaffold/src/flows/(external)/hoppscotch/auth.flow.ts +36 -0
  46. package/templates/scaffold/src/flows/(external)/hoppscotch/get.flow.ts +24 -0
  47. package/templates/scaffold/src/flows/(external)/hoppscotch/post.flow.ts +34 -0
  48. package/templates/scaffold/src/flows/(user-stories)/email-verification.flow.ts +47 -0
  49. package/templates/scaffold/src/flows/README.md +35 -14
  50. package/templates/scaffold/src/flows/{shop.flow.ts → home/cart.flow.ts} +18 -17
  51. package/templates/scaffold/src/flows/{example.flow.ts → home/hello.flow.ts} +12 -13
  52. package/templates/scaffold/tests/example.spec.ts +5 -5
  53. package/templates/scaffold/tests/hoppscotch.spec.ts +3 -3
  54. package/templates/scaffold/tests/mail-verify.spec.ts +7 -7
  55. package/templates/skills/waygraph-convention.skill.md +14 -6
  56. package/templates/scaffold/src/flows/hoppscotch-auth.flow.ts +0 -35
  57. package/templates/scaffold/src/flows/hoppscotch-get.flow.ts +0 -23
  58. package/templates/scaffold/src/flows/hoppscotch-post.flow.ts +0 -33
  59. package/templates/scaffold/src/flows/mail-verify.flow.ts +0 -46
package/dist/core/flow.js CHANGED
@@ -2,14 +2,25 @@
2
2
  import { connect } from "../types.js";
3
3
  import { chromiumExecutablePath, extensionPathsFromEnv, launchContext } from "../browser.js";
4
4
  import { resolveExtensions } from "../extensions.js";
5
+ import { loadWaygraphConfig } from "../config.js";
5
6
  import { WaygraphError, MSG_CHAINFLOW_BLOCK_NOT_FOUND_BY_INDEX, MSG_CHAINFLOW_BLOCK_NOT_FOUND_BY_NAME, MSG_CHAINFLOW_EMPTY, } from "../errors.js";
6
7
  import { preflight, runGraph } from "./run-graph.js";
7
8
  import { resolveLauncher } from "./config.js";
8
9
  import { MemPage } from "../mem-page.js";
9
- import { findBlockIndex, modVerify, withVerify } from "./block.js";
10
+ import { copyWaygraphRuntime, findBlockIndex, modVerify, withVerify } from "./block.js";
10
11
  import { normalizeHighlightSize, normalizeHighlightTone, normalizeHighlightWeight } from "../highlights.js";
11
12
  /** Launches the browser and context an Engine uses when the caller gave none, from its config. */
12
- async function openOwnContext(config) {
13
+ async function openOwnContext(engineConfig) {
14
+ // A config chosen with `--config` / WAYGRAPH_CONFIG wins over the one the Engine was built with, so a flow that imports
15
+ // an engine from one config file still follows the config the run selected.
16
+ const chosen = process.env.WAYGRAPH_CONFIG ? await loadWaygraphConfig(process.cwd()) : undefined;
17
+ const config = chosen
18
+ ? {
19
+ ...engineConfig,
20
+ ...(chosen.baseUrl !== undefined ? { baseUrl: chosen.baseUrl } : {}),
21
+ ...(chosen.extensions !== undefined ? { extensions: chosen.extensions } : {}),
22
+ }
23
+ : engineConfig;
13
24
  const browserName = config?.browserName ?? "chromium";
14
25
  const executablePath = browserName === "chromium" ? chromiumExecutablePath() : undefined;
15
26
  const baseURL = config?.baseUrl ?? process.env.WAYGRAPH_BASE_URL;
@@ -109,7 +120,12 @@ export function buildFlow(middle, engineConfig) {
109
120
  };
110
121
  return {
111
122
  resetSession: false,
112
- blocks: () => middle.map((b) => ({ name: b.name, block: b, ...(b.routes ? { routes: b.routes } : {}) })),
123
+ blocks: () => middle.map((b) => ({
124
+ name: b.name,
125
+ block: b,
126
+ ...(b.routes ? { routes: b.routes } : {}),
127
+ ...(b.__waygraphResetBefore ? { resetSessionBefore: true } : {}),
128
+ })),
113
129
  run: run,
114
130
  withBlockVerify(block, verify) {
115
131
  const index = findBlockIndex(middle, block);
@@ -139,6 +155,7 @@ export function buildFlow(middle, engineConfig) {
139
155
  * exactly as if a human had opened the app fresh, not wherever the
140
156
  * previous Flow in the chain happened to leave the page.
141
157
  * @example const loginFlow = withSessionReset(engine.defineFlow([start, LoginBlock, end]));
158
+ * @deprecated Use `map().flow(sub, { reset: true })`; the reset belongs to where flows are joined.
142
159
  */
143
160
  export function withSessionReset(flow) {
144
161
  return {
@@ -154,6 +171,7 @@ export function withSessionReset(flow) {
154
171
  * Blocks are executing, instead of one fixed title for the whole run.
155
172
  * Non-destructive, same pattern as {@link withSessionReset}.
156
173
  * @example const ticket123 = withTitle(loginFlow, "Ticket #123 - Sales rep signs in");
174
+ * @deprecated Name a flow with `ctx.title(...)` in a stub on its first step.
157
175
  */
158
176
  export function withTitle(flow, title) {
159
177
  return {
@@ -172,6 +190,7 @@ export function withTitle(flow, title) {
172
190
  * this itself - the CLI's `--data`/`--mem-stub` seeding, or a bare-library
173
191
  * caller's own explicit `seedMemStub(mem, flow)` call, does the filling.
174
192
  * @example const demoLogin = withMemStub(loginFlow);
193
+ * @deprecated Use `engine.map({ memStub: true })`.
175
194
  */
176
195
  export function withMemStub(flow) {
177
196
  return {
@@ -189,6 +208,7 @@ export function withMemStub(flow) {
189
208
  * @example withDemoPace(happyFlow, 0.5) // 2x faster than normal
190
209
  * @example withDemoPace(gapFlow, 2.5) // 2.5x slower
191
210
  * @example withDemoPace(gapFlow, 4500) // absolute ~4.5s gates/dwells
211
+ * @deprecated Use `engine.map({ demoPace })`, a step `{ pace }`, or `demo.pace` in the config.
192
212
  */
193
213
  export function withDemoPace(flow, pace) {
194
214
  const demoPace = pace;
@@ -203,6 +223,7 @@ export function withDemoPace(flow, pace) {
203
223
  * Default highlight look for a Flow episode (size / weight / optional tone).
204
224
  * Per-slot stub or {@link withHighlightFixtures} values win when set.
205
225
  * @example withHighlightStyle(withDemoPace(gapFlow, "slow"), { size: "lg", weight: "bold" })
226
+ * @deprecated Use `engine.map({ highlightStyle })`.
206
227
  */
207
228
  export function withHighlightStyle(flow, style) {
208
229
  const highlightStyle = {
@@ -220,12 +241,12 @@ export function withHighlightStyle(flow, style) {
220
241
  /**
221
242
  * Demo pacing for one Block or compose unit (not FF - use fastForwardComposeBlock for blitz).
222
243
  * @example withBlockPace(composeBlock("gap-review", [...]), "slow")
244
+ * @deprecated Use a step option: `.method(Block, { pace })`.
223
245
  */
224
246
  export function withBlockPace(block, pace) {
225
- return {
226
- ...block,
227
- demoPace: pace,
228
- };
247
+ const next = { ...block, demoPace: pace };
248
+ copyWaygraphRuntime(block, next);
249
+ return next;
229
250
  }
230
251
  /**
231
252
  * Marks a Flow as one that's SUPPOSED to throw - e.g. a locked-out user's
@@ -244,6 +265,7 @@ export function withBlockPace(block, pace) {
244
265
  * engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, end]),
245
266
  * "locked_out_user stays on LoginPage with the error banner - working as intended.",
246
267
  * );
268
+ * @deprecated Use `engine.map({ expectedFailure: reason })`.
247
269
  */
248
270
  export function withExpectedFailure(flow, reason) {
249
271
  return {
@@ -259,6 +281,7 @@ export function withExpectedFailure(flow, reason) {
259
281
  * stubOnError slots. Non-destructive, same pattern as {@link withTitle}.
260
282
  * Demo/`chain --step` reads `flow.highlightFixtures`; `run()` ignores it.
261
283
  * @example withHighlightFixtures(issueFlow, { "submit-login": { stubBefore: { email: { label: "AC-1" } } } })
284
+ * @deprecated Chain ctx stubs on the Block: `Block.stubAfter((ctx) => …)`.
262
285
  */
263
286
  export function withHighlightFixtures(flow, fixtures) {
264
287
  return {
@@ -306,7 +329,7 @@ function locateInChain(flows, block) {
306
329
  }
307
330
  throw new WaygraphError("WG_FLOW_BLOCK_NOT_FOUND", MSG_CHAINFLOW_BLOCK_NOT_FOUND_BY_NAME(blockName));
308
331
  }
309
- async function clearSessionState(context, page) {
332
+ export async function clearSessionState(context, page) {
310
333
  await context.clearCookies().catch(() => { });
311
334
  await page
312
335
  .evaluate(() => {
@@ -337,6 +360,7 @@ async function clearSessionState(context, page) {
337
360
  * chain - nothing to reset yet). The combined result is whatever the LAST
338
361
  * flow resolved to.
339
362
  * @example chainFlow(ticket123Flow, ticket456Flow).run(context, mem)
363
+ * @deprecated Nest flows with `map().flow(a).flow(b, { reset: true })`.
340
364
  */
341
365
  export function chainFlow(...flows) {
342
366
  if (flows.length === 0) {
@@ -1,6 +1,7 @@
1
1
  import type { Block, Checkpoint } from "../types.js";
2
2
  import type { EndMarker, StartMarker } from "./run-graph.js";
3
3
  import type { Flow, FlowOptions } from "./flow.js";
4
+ import type { DemoPace, HighlightStyleDefaults } from "../highlights.js";
4
5
  import type { NavBlock } from "./blocks/nav.js";
5
6
  import type { PageBlock } from "./blocks/page.js";
6
7
  import type { AssertBlock, MethodBlock } from "./blocks/method.js";
@@ -13,7 +14,6 @@ type S = Checkpoint<"__start__">;
13
14
  * defineFlow) - a real cycle check:structure forbids. This type is how that's avoided.
14
15
  */
15
16
  export type DefineFlowFn = (blocks: readonly [StartMarker, ...Block<any, any>[], EndMarker], options?: FlowOptions) => Flow<any>;
16
- /** Per-step options every `MapBuilder` step method (`.gotoPage()`/`.method()`/`.assert()`) takes. */
17
17
  export interface MapStepOpts {
18
18
  /**
19
19
  * Fast-forward just this step, inline - no `.ffStart()/.ffEnd()` bracket needed. Consecutive
@@ -22,6 +22,12 @@ export interface MapStepOpts {
22
22
  * @example map().gotoPage(NavLoginBlock, { ff: true }).method(FillUsernameBlock, { ff: true })
23
23
  */
24
24
  ff?: boolean;
25
+ /**
26
+ * Demo pace for just this step (`"blitz"` skips the typing animation and the pause after the action; `"fast"`, `"slow"`,
27
+ * or a number scale the narration gates). The map equivalent of `withBlockPace`, with no wrapper.
28
+ * @example map().method(SetBearerTokenBlock, { pace: "blitz" })
29
+ */
30
+ pace?: DemoPace;
25
31
  }
26
32
  export interface MapBuilderOptions {
27
33
  /**
@@ -31,6 +37,19 @@ export interface MapBuilderOptions {
31
37
  * way; only the internal/external origin distinction is opt-in).
32
38
  */
33
39
  homeOrigin?: string;
40
+ /** Fill missing `requires` keys from `registerMemStub` when the flow runs (what `withMemStub` did). */
41
+ memStub?: boolean;
42
+ /** Marks a flow that is supposed to throw, with the reason shown in the demo (what `withExpectedFailure` did). */
43
+ expectedFailure?: string;
44
+ /** Default demo pace for the whole flow (what `withDemoPace` did). */
45
+ demoPace?: DemoPace;
46
+ /** Default ring tone, size and weight for the whole flow (what `withHighlightStyle` did). */
47
+ highlightStyle?: HighlightStyleDefaults;
48
+ }
49
+ /** What `.flow()` takes for the sub-flow it runs inline. */
50
+ export interface MapFlowOpts {
51
+ /** Clear cookies and storage right before the sub-flow, e.g. to start a second user fresh (what `withSessionReset` + `chainFlow` did). */
52
+ reset?: boolean;
34
53
  }
35
54
  /**
36
55
  * Fluent builder over {@link Engine.defineFlow} - see `map()`'s own doc
@@ -119,6 +138,12 @@ export declare class MapBuilder<Out extends Checkpoint<string>> {
119
138
  method<NextOut extends Checkpoint<string>>(block: (MethodBlock<Out, NextOut> | EffectBlock<Out, NextOut>) & {
120
139
  name: string;
121
140
  }, opts?: MapStepOpts): MapBuilder<NextOut>;
141
+ /**
142
+ * Runs another flow's steps inline here, in the same page, browser and `mem`. This is how flows nest:
143
+ * `map().flow(loginFlow).flow(viewerFlow, { reset: true })`. With `{ reset: true }` cookies and storage are cleared
144
+ * right before the sub-flow's first step. Replaces `chainFlow` and `withSessionReset`.
145
+ */
146
+ flow<NextOut extends Checkpoint<string>>(sub: Flow<NextOut>, opts?: MapFlowOpts): MapBuilder<NextOut>;
122
147
  /**
123
148
  * Branch: run everything added so far, then continue into whichever `routes[tag]` matches the
124
149
  * resolved Checkpoint's tag - same page, same context, same mem. `routes` must cover every tag
@@ -166,6 +191,7 @@ export declare class MapBuilder<Out extends Checkpoint<string>> {
166
191
  * left open at `.end()` closes silently; an EXPLICIT `.ffStart()` left open
167
192
  * still throws - the author opened that one and needs to close it themselves.
168
193
  */
194
+ /** @param options @deprecated Pass flow settings to `map({ … })` instead. */
169
195
  end(options?: FlowOptions): Flow<Out>;
170
196
  }
171
197
  export {};
@@ -2,6 +2,30 @@ import { WaygraphError, MSG_MAP_WRONG_KIND, MSG_MAP_WRONG_SALT, MSG_MAP_WRONG_OR
2
2
  import { end, start } from "./run-graph.js";
3
3
  import { MemPage } from "../mem-page.js";
4
4
  import { fastForwardComposeBlock } from "./compose.js";
5
+ import { clearSessionState, withBlockPace } from "./flow.js";
6
+ import { copyWaygraphRuntime } from "./block.js";
7
+ /** Per-step options every `MapBuilder` step method (`.gotoPage()`/`.method()`/`.assert()`) takes. */
8
+ /** The same Block, but it clears cookies and storage before it acts. Keeps the runtime markers. */
9
+ function resetBefore(block) {
10
+ const act = block.instruction.act.bind(block.instruction);
11
+ const next = {
12
+ ...block,
13
+ instruction: {
14
+ ...block.instruction,
15
+ async act(page, input, mem) {
16
+ await clearSessionState(page.context(), page);
17
+ return act(page, input, mem);
18
+ },
19
+ },
20
+ };
21
+ copyWaygraphRuntime(block, next);
22
+ Object.defineProperty(next, "__waygraphResetBefore", { value: true, enumerable: false, configurable: true });
23
+ return next;
24
+ }
25
+ /** Applies a step's `pace` option, keeping the Block's runtime markers. */
26
+ function paced(block, opts) {
27
+ return (opts?.pace === undefined ? block : withBlockPace(block, opts.pace));
28
+ }
5
29
  function mapKindOf(block) {
6
30
  return block?.__waygraphKind;
7
31
  }
@@ -237,7 +261,7 @@ export class MapBuilder {
237
261
  gotoPage(block, opts) {
238
262
  assertMapKind(block, ["nav", "page"], "gotoPage");
239
263
  assertMapOrigin(block, this.homeOrigin, "internal", "gotoPage");
240
- return this.appendStep(block, opts?.ff);
264
+ return this.appendStep(paced(block, opts), opts?.ff);
241
265
  }
242
266
  /**
243
267
  * Appends a genuinely cross-origin navigation step (e.g. `(external)/`
@@ -249,7 +273,7 @@ export class MapBuilder {
249
273
  gotoExternal(block, opts) {
250
274
  assertMapKind(block, ["nav", "page"], "gotoExternal");
251
275
  assertMapOrigin(block, this.homeOrigin, "external", "gotoExternal");
252
- return this.appendStep(block, opts?.ff);
276
+ return this.appendStep(paced(block, opts), opts?.ff);
253
277
  }
254
278
  /**
255
279
  * Appends a self-loop verification step - requires a Block from
@@ -258,7 +282,7 @@ export class MapBuilder {
258
282
  */
259
283
  assert(block, opts) {
260
284
  assertMapKind(block, ["assert"], "assert");
261
- return this.appendStep(block, opts?.ff);
285
+ return this.appendStep(paced(block, opts), opts?.ff);
262
286
  }
263
287
  /**
264
288
  * Appends a non-navigating page action (submit, upload, add/remove an
@@ -274,7 +298,20 @@ export class MapBuilder {
274
298
  throw new WaygraphError("WG_MAP_WRONG_KIND", MSG_MAP_METHOD_GOT_ASSERT(block.name));
275
299
  }
276
300
  assertMapSalt(block, ["method", "effect"], "method");
277
- return this.appendStep(block, opts?.ff);
301
+ return this.appendStep(paced(block, opts), opts?.ff);
302
+ }
303
+ /**
304
+ * Runs another flow's steps inline here, in the same page, browser and `mem`. This is how flows nest:
305
+ * `map().flow(loginFlow).flow(viewerFlow, { reset: true })`. With `{ reset: true }` cookies and storage are cleared
306
+ * right before the sub-flow's first step. Replaces `chainFlow` and `withSessionReset`.
307
+ */
308
+ flow(sub, opts) {
309
+ let next = this;
310
+ sub.blocks().forEach((info, i) => {
311
+ const block = i === 0 && opts?.reset ? resetBefore(info.block) : info.block;
312
+ next = next.appendStep(block);
313
+ });
314
+ return next;
278
315
  }
279
316
  /**
280
317
  * Branch: run everything added so far, then continue into whichever `routes[tag]` matches the
@@ -329,6 +366,7 @@ export class MapBuilder {
329
366
  * left open at `.end()` closes silently; an EXPLICIT `.ffStart()` left open
330
367
  * still throws - the author opened that one and needs to close it themselves.
331
368
  */
369
+ /** @param options @deprecated Pass flow settings to `map({ … })` instead. */
332
370
  end(options) {
333
371
  if (this.ff && !this.ff.auto) {
334
372
  throw new WaygraphError("WG_MAP_FF_NOT_CLOSED", MSG_MAP_FF_NOT_CLOSED(this.ff.name));
@@ -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
@@ -9,7 +9,7 @@ export { Trait, urlMatches, textEquals, visible, disabled, enabled, frameVisible
9
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
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";
@@ -30,7 +30,7 @@ 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, ConfigContext } from "./config.js";
33
+ export type { WaygraphConfig, ConfigContext, VideoSettings, RunSettings, DemoSettings } from "./config.js";
34
34
  export { withWebServer } from "./webserver.js";
35
35
  export { chromiumExecutablePath } from "./browser.js";
36
36
  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,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 {};
@@ -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
  }
@@ -231,6 +231,9 @@ async function runMain() {
231
231
  : "waygraph demo: opaque FFCompose in flow -> Playwright slowMo=0 (blitz); set WAYGRAPH_SLOWMO to override",
232
232
  );
233
233
  }
234
+ // Step (demo) mode: slowMo is waygraph's own beat after each action, not Playwright's per-operation slowMo, so a
235
+ // step's pace can remove it and typing is not taxed per keystroke. The browser launches at 0.
236
+ const launchSlowMo = step ? 0 : slowMo;
234
237
  const baseURL = process.env.WAYGRAPH_BASE_URL;
235
238
  const title = process.env.WAYGRAPH_TITLE;
236
239
  // Comma-separated Block names (their real .name, e.g. "login") that
@@ -263,7 +266,7 @@ async function runMain() {
263
266
  const fixedVideoViewport = videoDir
264
267
  ? (videoViewportParsed ?? (step ? DEFAULT_VIDEO_VIEWPORT : DEFAULT_RUN_VIDEO_VIEWPORT))
265
268
  : null;
266
- const engine = new Engine({ headless: !headed, slowMo });
269
+ const engine = new Engine({ headless: !headed, slowMo: launchSlowMo });
267
270
  let result;
268
271
  // Non-interactive execution (no --step) runs the whole chain as one
269
272
  // composed flow with no per-block loop of its own to defer seeding
@@ -312,7 +315,7 @@ async function runMain() {
312
315
  launcher: chromium,
313
316
  launch: {
314
317
  headless: !headed,
315
- slowMo,
318
+ slowMo: launchSlowMo,
316
319
  args: step || fixedVideoViewport ? ["--start-maximized"] : [],
317
320
  ...(executablePath ? { executablePath } : {}),
318
321
  },
@@ -1,7 +1,7 @@
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 { applyHighlightStyleDefaults, demoPaceGateMs, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, formatHighlightCaption, hasAuthoredStubAfter, resolveSlides, resolveStepDemoPace, resolveTodoDockUi, runStubPhase } from "../highlights.js";
4
+ import { applyHighlightStyleDefaults, normalizeDemoPace, 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
7
  import { extractVerifyHighlights, presentFailPanel, renderAfterStep, renderBeforeStep } from "./step-panels.js";
@@ -26,6 +26,8 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
26
26
  const demoFast = process.env.WAYGRAPH_DEMO_FAST === "1";
27
27
  const stepperMode = process.env.WAYGRAPH_STEPPER === "full" ? "full" : "carousel";
28
28
  const pacing = { gatesFast: demoFast, gatesSlow: false, skipTheater: false, demoPace: "normal" };
29
+ // A title set by a stub (`ctx.title`) names the flow from that step on, until another stub sets a new one.
30
+ let stickyTitle;
29
31
  const stubBeforeRef = { current: [] };
30
32
  /** Live todo dock for Method fill/click advance (same carry as stage.state.lastTodoDock). */
31
33
  const todoDockRef = { current: undefined };
@@ -213,15 +215,18 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
213
215
  }
214
216
  // Episode pace: block > flow > CLI --fast (only when no authored pace).
215
217
  // Authoring wins: withDemoPace("slow"|2|4500) keeps slow dwell even under --fast.
218
+ // `demo.pace` in the config (WAYGRAPH_DEMO_PACE) is the default for flows and steps that set none.
219
+ const defaultPace = process.env.WAYGRAPH_DEMO_PACE ? normalizeDemoPace(process.env.WAYGRAPH_DEMO_PACE) : undefined;
216
220
  const stepPace = resolveStepDemoPace({
217
221
  blockPace: r.block.demoPace,
218
- flowPace: r.demoPace,
222
+ flowPace: r.demoPace ?? defaultPace,
219
223
  fastForward: !!r.block.fastForward,
220
224
  wasFastForward: !!r.wasFastForward,
221
225
  });
222
226
  const authoredPace =
223
227
  (r.block.demoPace != null && r.block.demoPace !== "") ||
224
- (r.demoPace != null && r.demoPace !== "");
228
+ (r.demoPace != null && r.demoPace !== "") ||
229
+ defaultPace != null;
225
230
  const cliFast = demoFast || fastBlockNames.has(r.block.name);
226
231
  pacing.demoPace =
227
232
  stepPace === "normal" && cliFast && !authoredPace
@@ -266,8 +271,9 @@ export async function runStepMode(engine, start, end, context, page, mem, resolv
266
271
  (stage.state.lastTodoDock && stage.state.lastTodoDock.groups[0] && stage.state.lastTodoDock.groups[0].items) ||
267
272
  stubBeforePhase.todos ||
268
273
  [];
274
+ if (stubBeforePhase.title && String(stubBeforePhase.title).trim()) stickyTitle = String(stubBeforePhase.title).trim();
269
275
  const overlayTitle =
270
- (stubBeforePhase.title && String(stubBeforePhase.title).trim()) ||
276
+ stickyTitle ||
271
277
  (r.episodeTitle && String(r.episodeTitle).trim()) ||
272
278
  title ||
273
279
  "waygraph demo";
@@ -24,7 +24,7 @@ export interface ExploreEdge extends WaygraphEdge {
24
24
  label?: string;
25
25
  instanceOption?: WaygraphInstanceOption;
26
26
  }
27
- export interface ExploreSection {
27
+ interface ExploreSection {
28
28
  title: string;
29
29
  edges: ExploreEdge[];
30
30
  }
@@ -55,3 +55,4 @@ export declare function buildExploreContext(projectDir: string, opts?: ExploreCo
55
55
  graph: WaygraphGraph;
56
56
  library: Awaited<ReturnType<typeof loadBlockLibrary>>;
57
57
  }>;
58
+ export {};
@@ -1,6 +1,6 @@
1
1
  import { AutoSession, type AutoSessionInit, type SessionSnapshot, type InspectDomOptions, type DomSnapshot, type TraceStep, type ApplyPathResult, type ConsoleLogEntry, type StorageSnapshot, type StubFileKind } from "../auto-session.js";
2
2
  import type { PilotHighlightFixtures } from "../pilot-overlay.js";
3
- export type { PilotHighlightFixtures, PilotFixtureRing } from "../pilot-overlay.js";
3
+ export type { PilotHighlightFixtures } from "../pilot-overlay.js";
4
4
  export interface SessionMeta {
5
5
  sessionId: string;
6
6
  socketPath: string;
@@ -54,7 +54,6 @@ export type HighlightResponse = {
54
54
  ok: false;
55
55
  error: string;
56
56
  };
57
- export declare function readSessionMeta(projectDir: string, sessionId: string): SessionMeta | null;
58
57
  /**
59
58
  * Resolve session metadata from a project hint (cwd) or the global registry.
60
59
  * Fixes "session unreachable" when the agent calls status/send from a different
@@ -58,7 +58,7 @@ function logPathFor(projectDir, sessionId) {
58
58
  function generateSessionId() {
59
59
  return randomBytes(4).toString("hex");
60
60
  }
61
- export function readSessionMeta(projectDir, sessionId) {
61
+ function readSessionMeta(projectDir, sessionId) {
62
62
  const p = metaPath(projectDir, sessionId);
63
63
  if (!existsSync(p))
64
64
  return null;
@@ -1,21 +1,8 @@
1
- /**
2
- * This package's own installed root (contains `templates/`/`examples/`, sibling to `dist/`) -
3
- * this file compiles to `dist/session/block-inject.js`, two levels down, not one. Same class of
4
- * bug as `cli/util.ts`'s own `packageRoot()` (see its comment) - self-inflicted here by moving
5
- * this file into `src/session/` without updating the path math to match the new depth.
6
- */
7
- export declare function packageRoot(): string;
8
- /** Human label for an injected root (used in merged `file` paths). */
9
- export declare function injectLabel(root: string): string;
10
1
  /**
11
2
  * Display path for a Block discovered under an inject root (not the host cwd).
12
3
  * Primary project files stay relative as-is; injected ones are tagged `@label/...`.
13
4
  */
14
5
  export declare function injectTaggedFile(hostDir: string, root: string, relFile: string): string;
15
- /**
16
- * Resolve one `--inject` token to an absolute directory (preset name or path).
17
- */
18
- export declare function resolveInjectRoot(token: string, cwd?: string): string;
19
6
  export declare function resolveInjectRoots(tokens: string[], cwd?: string): string[];
20
7
  /** Roots to scan: host project first, then each inject path (deduped). */
21
8
  export declare function exploreRoots(projectDir: string, inject?: string[]): string[];
@@ -12,11 +12,11 @@ import { homedir } from "node:os";
12
12
  * bug as `cli/util.ts`'s own `packageRoot()` (see its comment) - self-inflicted here by moving
13
13
  * this file into `src/session/` without updating the path math to match the new depth.
14
14
  */
15
- export function packageRoot() {
15
+ function packageRoot() {
16
16
  return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
17
17
  }
18
18
  /** Human label for an injected root (used in merged `file` paths). */
19
- export function injectLabel(root) {
19
+ function injectLabel(root) {
20
20
  return basename(root.replace(/\/$/, ""));
21
21
  }
22
22
  /**
@@ -50,7 +50,7 @@ const PRESETS = {
50
50
  /**
51
51
  * Resolve one `--inject` token to an absolute directory (preset name or path).
52
52
  */
53
- export function resolveInjectRoot(token, cwd = process.cwd()) {
53
+ function resolveInjectRoot(token, cwd = process.cwd()) {
54
54
  const raw = token.trim();
55
55
  if (!raw)
56
56
  throw new Error("waygraph inject: empty --inject value");
@@ -29,7 +29,6 @@
29
29
  * new execution primitive.
30
30
  */
31
31
  import { type WaygraphGraph } from "../analysis/graph.js";
32
- import { listBrowserSessions, type SessionMeta } from "./auto-session-ipc.js";
33
32
  import { type BrowserStartInit } from "./browser.js";
34
33
  import type { SessionSnapshot } from "../auto-session.js";
35
34
  export interface PilotStartResult {
@@ -51,4 +50,3 @@ export interface PilotStartResult {
51
50
  export declare function pilotStart(init: BrowserStartInit): Promise<PilotStartResult>;
52
51
  /** Reattach to an existing browser session — does not spawn a new browser. */
53
52
  export declare function pilotAttach(projectDir: string, sessionId: string, inject?: string[]): Promise<PilotStartResult>;
54
- export { listBrowserSessions, type SessionMeta };