waygraph 0.16.1 → 0.16.2

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 (55) hide show
  1. package/dist/cli/commands/codemod-stubs.js +90 -2
  2. package/dist/cli/run-chain.js +2 -2
  3. package/dist/cli/util.js +1 -1
  4. package/dist/demo/narration/demo-pace.d.ts +1 -1
  5. package/dist/demo/narration/demo-pace.js +1 -1
  6. package/dist/demo/narration/merge.d.ts +1 -1
  7. package/dist/demo/narration/merge.js +1 -1
  8. package/dist/demo/narration/narration.d.ts +2 -6
  9. package/dist/demo/narration/narration.js +5 -17
  10. package/dist/demo/narration/stub-ctx.js +3 -3
  11. package/dist/demo/overlay/inpage/core.js +0 -6
  12. package/dist/demo/overlay/overlay-css.js +1 -55
  13. package/dist/demo/overlay/overlay.js +3 -2
  14. package/dist/demo/overlay/rings.d.ts +0 -8
  15. package/dist/demo/overlay/rings.js +0 -15
  16. package/dist/demo/overlay/ui/overlay.css +40 -0
  17. package/dist/demo/stepper/after.js +4 -17
  18. package/dist/demo/stepper/before.js +3 -1
  19. package/dist/demo/stepper/demo-log.js +1 -1
  20. package/dist/demo/stepper/interaction-patch.js +3 -15
  21. package/dist/demo/stepper/panel-after.js +1 -1
  22. package/dist/demo/stepper/slides.d.ts +3 -0
  23. package/dist/demo/stepper/slides.js +18 -2
  24. package/dist/demo/stepper/teardown.js +0 -1
  25. package/dist/demo/verbose-log.js +1 -1
  26. package/dist/engine/block/block.d.ts +1 -12
  27. package/dist/engine/block/kinds/method.d.ts +0 -2
  28. package/dist/engine/block/kinds/method.js +0 -1
  29. package/dist/engine/block/kinds/nav.js +1 -1
  30. package/dist/engine/run/run.d.ts +1 -19
  31. package/dist/engine/run/run.js +0 -88
  32. package/dist/engine/stub/ring.d.ts +2 -4
  33. package/dist/engine/stub/ring.js +1 -0
  34. package/dist/engine/stub/stub.d.ts +16 -8
  35. package/dist/index.d.ts +2 -3
  36. package/dist/index.js +2 -2
  37. package/dist/runner/plan/find.js +2 -2
  38. package/dist/runner/plan/plan.js +1 -1
  39. package/dist/runner/plan/seed-mem.js +5 -5
  40. package/dist/runner/plan/spec.js +1 -1
  41. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
  42. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +2 -1
  43. package/examples/saucedemo/src/flows/checkout.flow.ts +2 -1
  44. package/examples/saucedemo/src/flows/shop.flow.ts +2 -1
  45. package/examples/saucedemo/src/stubs/device-showcase.ts +37 -43
  46. package/examples/saucedemo/src/stubs/finish-order.ts +38 -0
  47. package/package.json +1 -1
  48. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
  49. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +2 -1
  50. package/templates/quickstart/src/flows/checkout.flow.ts +2 -1
  51. package/templates/quickstart/src/flows/shop.flow.ts +2 -1
  52. package/templates/quickstart/src/stubs/device-showcase.ts +37 -43
  53. package/templates/quickstart/src/stubs/finish-order.ts +38 -0
  54. package/templates/scaffold/src/flows/home/hello.flow.ts +3 -0
  55. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-hello.method.block.ts +1 -9
@@ -4,7 +4,9 @@
4
4
  // aliases (`banner`, `bannerPos`, `clearDevice`, `clearTodos`, `landscape`, `portrait`, `todoDockFull`) become the
5
5
  // names they stood for. A stub written as a slot map (`{ email: { selector, label } }`) becomes a function of
6
6
  // `ctx.ring` calls, and a stub set in a `define*Block({ ... })` (options or instruction) moves out to a chained
7
- // `.stubBefore(...)` on the Block. Works on the TypeScript AST (ts-morph).
7
+ // `.stubBefore(...)` on the Block; a Block's `slides` become `ctx.slides(...)` in its chained `.stubAfter(...)`;
8
+ // `ctx.highlights({ a: ring })` becomes one `ctx.ring("a", ring)` per ring; `narrate(locator, caption, action)` in an
9
+ // `act` becomes the action, reported so the caption can move to a stubBefore ring. Works on the TypeScript AST (ts-morph).
8
10
  import { Node, SyntaxKind } from "ts-morph";
9
11
  const RENAMED = {
10
12
  banner: (a) => `title(${a.join(", ")})`,
@@ -127,7 +129,7 @@ function convertSlotMaps(sf) {
127
129
  }
128
130
  return changed;
129
131
  }
130
- /** `defineXBlock({ ..., stubBefore: fn })` (or in its instruction) -> `defineXBlock({ ... }).stubBefore(fn)`. */
132
+ /** `defineXBlock({ ..., stubBefore: fn, slides })` (or in its instruction) -> `defineXBlock({ ... }).stubBefore(fn).stubAfter(slides)`. */
131
133
  function hoistDefinitionStubs(sf) {
132
134
  let changed = false;
133
135
  for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression).reverse()) {
@@ -141,9 +143,17 @@ function hoistDefinitionStubs(sf) {
141
143
  if (Node.isPropertyAssignment(instruction) && Node.isObjectLiteralExpression(instruction.getInitializer()))
142
144
  holders.push(instruction.getInitializer());
143
145
  const chained = [];
146
+ let slides;
144
147
  for (const holder of holders) {
145
148
  for (const prop of holder.getProperties()) {
146
149
  const name = Node.isPropertyAssignment(prop) || Node.isMethodDeclaration(prop) ? prop.getName() : "";
150
+ if (name === "slides" && Node.isPropertyAssignment(prop)) {
151
+ const init = prop.getInitializerOrThrow();
152
+ // `slides: (out) => [...]` read the Checkpoint the step reached; in stubAfter that is ctx.out.
153
+ slides = Node.isArrowFunction(init) || Node.isFunctionExpression(init) ? `(${init.getText()})(ctx.out!)` : init.getText();
154
+ prop.remove();
155
+ continue;
156
+ }
147
157
  if (!PHASES.has(name))
148
158
  continue;
149
159
  let fn;
@@ -157,6 +167,15 @@ function hoistDefinitionStubs(sf) {
157
167
  prop.remove();
158
168
  }
159
169
  }
170
+ if (slides !== undefined) {
171
+ const after = chained.findIndex((c) => c.startsWith(".stubAfter("));
172
+ if (after < 0)
173
+ chained.push(`.stubAfter((ctx) => ctx.slides(${slides}))`);
174
+ else {
175
+ const fn = chained[after].slice(".stubAfter(".length, -1);
176
+ chained[after] = `.stubAfter(async (ctx) => {\n await (${fn})(ctx);\n ctx.slides(${slides});\n})`;
177
+ }
178
+ }
160
179
  if (chained.length) {
161
180
  call.replaceWithText(call.getText() + chained.join(""));
162
181
  changed = true;
@@ -164,6 +183,71 @@ function hoistDefinitionStubs(sf) {
164
183
  }
165
184
  return changed;
166
185
  }
186
+ /** `ctx.highlights({ a: ring, b: ring })` -> `ctx.ring("a", ring); ctx.ring("b", ring);`, one call at a time. */
187
+ function convertHighlights(sf, problems) {
188
+ let changed = false;
189
+ const skipped = new Set();
190
+ for (;;) {
191
+ // Raw text replacement forgets the file's nodes, so look the next call up again each time.
192
+ const statement = sf.getDescendantsOfKind(SyntaxKind.ExpressionStatement).find((st) => {
193
+ const c = ctxCall(st);
194
+ return c && c.method === "highlights" && !skipped.has(st.getStart()) && isStubCtx(c.call, c.receiver);
195
+ });
196
+ if (!statement)
197
+ return changed;
198
+ const c = ctxCall(statement);
199
+ const map = c.call.getArguments()[0];
200
+ if (!Node.isObjectLiteralExpression(map)) {
201
+ problems.push({ line: c.call.getStartLineNumber(), reason: "ctx.highlights(...) was removed and its argument is not an object literal: call ctx.ring(id, ring) for each ring" });
202
+ skipped.add(statement.getStart());
203
+ continue;
204
+ }
205
+ // Each ring keeps its own lines, moved to the statement's indent (replaceWithText would re-indent them).
206
+ const column = (n) => n.getStart() - n.getStartLinePos();
207
+ const indent = " ".repeat(column(statement));
208
+ const rings = map.getProperties().flatMap((p) => {
209
+ if (!Node.isPropertyAssignment(p))
210
+ return [];
211
+ const from = column(p);
212
+ const value = p.getInitializerOrThrow().getText().split("\n").map((l, n) => (n === 0 ? l : indent + l.slice(Math.min(from, l.search(/\S|$/))))).join("\n");
213
+ return [`${c.receiver}.ring(${JSON.stringify(p.getName().replace(/^["']|["']$/g, ""))}, ${value});`];
214
+ });
215
+ sf.replaceText([statement.getStart(), statement.getEnd()], rings.join("\n" + indent));
216
+ changed = true;
217
+ }
218
+ }
219
+ /**
220
+ * `narrate(locator, caption, () => action)` in an `act` -> just the action. The caption was narration inside the
221
+ * Block; it belongs in a `stubBefore` ring where the flow uses the Block, which the codemod can't place, so it is
222
+ * reported.
223
+ */
224
+ function unwrapNarrate(sf, problems) {
225
+ let changed = false;
226
+ for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression).reverse()) {
227
+ if (call.wasForgotten() || call.getExpression().getText() !== "narrate" || call.getArguments().length !== 3)
228
+ continue;
229
+ const [, captionNode, action] = call.getArguments();
230
+ const caption = captionNode.getText();
231
+ const line = call.getStartLineNumber();
232
+ let body = `(${action.getText()})()`;
233
+ if (Node.isArrowFunction(action) && !Node.isBlock(action.getBody()))
234
+ body = action.getBody().getText();
235
+ call.replaceWithText(body);
236
+ problems.push({ line, reason: `narrate(...) was removed; its caption ${caption} is gone: ring the element in a stubBefore where the flow uses this Block` });
237
+ changed = true;
238
+ }
239
+ if (changed) {
240
+ for (const decl of sf.getImportDeclarations()) {
241
+ const spec = decl.getNamedImports().find((n) => n.getName() === "narrate");
242
+ if (!spec)
243
+ continue;
244
+ spec.remove();
245
+ if (decl.getNamedImports().length === 0 && !decl.getDefaultImport() && !decl.getNamespaceImport())
246
+ decl.remove();
247
+ }
248
+ }
249
+ return changed;
250
+ }
167
251
  /** Rewrites the old stub forms in every file; returns the files it changed and the calls left for a person. */
168
252
  export function convertStubs(project) {
169
253
  const changed = [];
@@ -171,6 +255,10 @@ export function convertStubs(project) {
171
255
  for (const sf of project.getSourceFiles()) {
172
256
  const left = [];
173
257
  let touched = hoistDefinitionStubs(sf);
258
+ if (unwrapNarrate(sf, left))
259
+ touched = true;
260
+ if (convertHighlights(sf, left))
261
+ touched = true;
174
262
  if (convertSlotMaps(sf))
175
263
  touched = true;
176
264
  for (const block of sf.getDescendantsOfKind(SyntaxKind.Block).reverse()) {
@@ -92,12 +92,12 @@ export async function runChainAuto(projectDir, fromTag, toTag) {
92
92
  const graph = await discoverGraph(projectDir);
93
93
  const path = findBlockPath(graph, fromTag, toTag);
94
94
  if (!path) {
95
- console.error(`waygraph chain auto: no Block path from "${fromTag}" to "${toTag}" in the discovered graph`);
95
+ console.error(`waygraph auto --blocks: no Block path from "${fromTag}" to "${toTag}" in the discovered graph`);
96
96
  process.exitCode = 1;
97
97
  return;
98
98
  }
99
99
  const spec = path.join(" then ");
100
- console.error(`waygraph chain auto: ${fromTag} -> ${toTag} via ${spec}`);
100
+ console.error(`waygraph auto --blocks: ${fromTag} -> ${toTag} via ${spec}`);
101
101
  if (!envString("WAYGRAPH_BASE_URL")) {
102
102
  const resolved = await resolveBaseUrl(projectDir);
103
103
  if (resolved)
package/dist/cli/util.js CHANGED
@@ -4,7 +4,7 @@ import { spawn } from "node:child_process";
4
4
  // ---------------------------------------------------------------------------
5
5
  // `try` - a bundled, self-contained showcase. No .flow.ts to write, no
6
6
  // scenario to design - copies a small real Flow (nav/action Blocks, Trait
7
- // verify, narrate()) into the target project and runs it in step mode
7
+ // verify, ctx stubs) into the target project and runs it in step mode
8
8
  // against a public demo site, so a curious dev sees the real overlay/panel
9
9
  // experience in under a minute, then gets pointed at the plain waygraph
10
10
  // source that produced it.
@@ -29,7 +29,7 @@ export declare function formatDemoPaceBadge(pace: DemoPace, baseAutoplayMs?: num
29
29
  */
30
30
  export declare function formatDemoPaceLabel(pace: DemoPace, baseAutoplayMs?: number): string;
31
31
  /**
32
- * Resolve min dwell ms for any fixture (stub / flow fixture / yap slide),
32
+ * Resolve min dwell ms for any fixture (a stub ring or a yap slide),
33
33
  * or `null` when duration is unset (caller keeps its default timing).
34
34
  *
35
35
  * Authoring wins: an explicit slow/numeric pace is not crushed by CLI `--fast`
@@ -150,7 +150,7 @@ export function formatDemoPaceLabel(pace, baseAutoplayMs = 1800) {
150
150
  return `pace normal (1x, ~${gate}ms gates)`;
151
151
  }
152
152
  /**
153
- * Resolve min dwell ms for any fixture (stub / flow fixture / yap slide),
153
+ * Resolve min dwell ms for any fixture (a stub ring or a yap slide),
154
154
  * or `null` when duration is unset (caller keeps its default timing).
155
155
  *
156
156
  * Authoring wins: an explicit slow/numeric pace is not crushed by CLI `--fast`
@@ -4,7 +4,7 @@ import type { SomeBlock } from "../../engine/block/block.js";
4
4
  import type { Checkpoint } from "../../engine/checkpoint/checkpoint.js";
5
5
  export declare function applyDefaultZoom(slots: ResolvedHighlight[], defaultZoom?: number): ResolvedHighlight[];
6
6
  export declare function applyDefaultZoomOut(slots: ResolvedHighlight[], defaultZoomOut?: boolean): ResolvedHighlight[];
7
- /** A stub phase's slot map as an ordered list of resolved highlights. */
7
+ /** A stub phase's rings (by id, as ctx.ring set them) as an ordered list of resolved highlights. */
8
8
  export declare function resolvePhaseMap(phaseMap: HighlightStubPhase): ResolvedHighlight[];
9
9
  /** The stub a Block's flow chained for this phase, if any. */
10
10
  export declare function readStubRaw(block: SomeBlock, phase: HighlightStubPhaseName): StubLifecycleFn<Checkpoint<string>> | undefined;
@@ -40,7 +40,7 @@ export function applyDefaultZoomOut(slots, defaultZoomOut) {
40
40
  return slots;
41
41
  return slots.map((h) => h.zoomOut !== undefined ? h : { ...h, zoomOut: defaultZoomOut });
42
42
  }
43
- /** A stub phase's slot map as an ordered list of resolved highlights. */
43
+ /** A stub phase's rings (by id, as ctx.ring set them) as an ordered list of resolved highlights. */
44
44
  export function resolvePhaseMap(phaseMap) {
45
45
  const phaseKeys = Object.keys(phaseMap);
46
46
  const allNumeric = phaseKeys.length > 0 && phaseKeys.every((k) => Number.isFinite(Number(k)) && String(Number(k)) === k);
@@ -1,10 +1,10 @@
1
1
  import type { HighlightStubPhaseName, StubPhaseResult } from "../../engine/stub/stub.js";
2
- import type { ResolvedHighlight, WaygraphSlide } from "../../engine/stub/ring.js";
2
+ import type { ResolvedHighlight } from "../../engine/stub/ring.js";
3
3
  import type { SomeBlock } from "../../engine/block/block.js";
4
4
  import type { Checkpoint } from "../../engine/checkpoint/checkpoint.js";
5
5
  import type { MemPage } from "../../engine/mem/mem.js";
6
6
  /**
7
- * Run an open stub lifecycle (preferred) or resolve a static slot map.
7
+ * Runs a Block's stub for one phase and returns what it painted.
8
8
  * Returns rings + episode fixtures (todos / todoIndex / zoom).
9
9
  *
10
10
  * @example
@@ -23,10 +23,6 @@ export declare function runStubPhase(block: SomeBlock, phase: HighlightStubPhase
23
23
  export declare function resolveHighlightSlots(block: SomeBlock, phase: HighlightStubPhaseName, opts?: {
24
24
  out?: Checkpoint<string>;
25
25
  }): ResolvedHighlight[];
26
- /** Resolve multi-step yap slides from `instruction.slides` (function form allowed). */
27
- export declare function resolveSlides(block: SomeBlock, opts?: {
28
- out?: Checkpoint<string>;
29
- }): WaygraphSlide[];
30
26
  /**
31
27
  * True when the block authored stubAfter (lifecycle fn or slots).
32
28
  * When true, demo should not use verify-trait fallback.
@@ -3,7 +3,7 @@ import { applyDefaultZoom, applyDefaultZoomOut, readStubRaw, resolvePhaseMap } f
3
3
  import { buildTodoDock, normalizeTodoStyle, normalizeTodos } from "./todo-dock.js";
4
4
  import { resolveDeviceState } from "./device.js";
5
5
  /**
6
- * Run an open stub lifecycle (preferred) or resolve a static slot map.
6
+ * Runs a Block's stub for one phase and returns what it painted.
7
7
  * Returns rings + episode fixtures (todos / todoIndex / zoom).
8
8
  *
9
9
  * @example
@@ -27,6 +27,9 @@ export async function runStubPhase(block, phase, opts) {
27
27
  throw new Error(`${block.name}: its ${phase} stub threw: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
28
28
  }
29
29
  }
30
+ if (phase === "stubOnError" && bag.slides?.length) {
31
+ throw new Error(`${block.name}: its stubOnError stub set slides; slides play before or after a step (stubBefore / stubAfter), not on its failure`);
32
+ }
30
33
  let highlights = applyDefaultZoom(resolvePhaseMap(bag.highlights), bag.zoom);
31
34
  highlights = applyDefaultZoomOut(highlights, bag.zoomOut);
32
35
  const todoDock = bag.hideTodos
@@ -95,6 +98,7 @@ export async function runStubPhase(block, phase, opts) {
95
98
  // Boolean, not string - "" isn't a state here, so a plain undefined-check (like zoom/title)
96
99
  // is enough; false is real ("author called hideNetworkTab()") and must still be forwarded.
97
100
  ...(bag.networkTabVisible !== undefined ? { networkTabVisible: bag.networkTabVisible } : {}),
101
+ slides: [...(bag.slides ?? [])],
98
102
  ...(bag.todoPos !== undefined ? { todoPos: bag.todoPos } : {}),
99
103
  ...(bag.todoDockUi !== undefined ? { todoDockUi: bag.todoDockUi } : {}),
100
104
  ...(bag.todoParallel === true ? { todoParallel: true } : {}),
@@ -118,22 +122,6 @@ export function resolveHighlightSlots(block, phase, opts) {
118
122
  }
119
123
  return applyDefaultZoom(resolvePhaseMap(bag.highlights), bag.zoom);
120
124
  }
121
- /** Resolve multi-step yap slides from `instruction.slides` (function form allowed). */
122
- export function resolveSlides(block, opts) {
123
- const instr = block.instruction;
124
- let raw = instr.slides;
125
- if (typeof raw === "function") {
126
- try {
127
- raw = raw(opts?.out ?? { __state: "" });
128
- }
129
- catch {
130
- raw = [];
131
- }
132
- }
133
- if (!Array.isArray(raw))
134
- return [];
135
- return raw.filter((s) => s && typeof s.caption === "string" && s.caption.length > 0);
136
- }
137
125
  function stubPhaseLooksAuthored(block, phase) {
138
126
  return readStubRaw(block, phase) !== undefined;
139
127
  }
@@ -25,9 +25,6 @@ export function createStubCtx(bag, opts) {
25
25
  mem: opts.mem,
26
26
  out: opts.out,
27
27
  error: opts.error,
28
- highlights(slots) {
29
- bag.highlights = { ...(slots || {}) };
30
- },
31
28
  ring(id, stub) {
32
29
  bag.highlights = { ...bag.highlights, [id]: stub };
33
30
  },
@@ -134,6 +131,9 @@ export function createStubCtx(bag, opts) {
134
131
  caption(text) {
135
132
  bag.caption = String(text ?? "");
136
133
  },
134
+ slides(slides) {
135
+ bag.slides = slides;
136
+ },
137
137
  hideCaption() {
138
138
  bag.caption = "";
139
139
  },
@@ -275,12 +275,6 @@ export function installCore({ envAutoplay, todoDockUi }) {
275
275
  pulse.classList.add("wg-pulse");
276
276
  };
277
277
  window.__wgHideRing = (opts) => {
278
- // While a narrate() call owns the ring (mid multi-step action),
279
- // the auto-highlight click/fill patches' own end-of-step hide is
280
- // a no-op - narrate() itself clears it once the WHOLE wrapped
281
- // action finishes, not just its first sub-step.
282
- if (window.__wgNarrateOwnsRing)
283
- return;
284
278
  if (window.__wgStopRingFollow)
285
279
  window.__wgStopRingFollow();
286
280
  const ring = __wgById("wg-ring");
@@ -1,7 +1,7 @@
1
1
  // CSS for the runner-only chrome (step panel, chips) and for the host page (device stage), next to the shared bundle in ui/css.
2
2
  import { SURFACE } from "./ui/tokens.js";
3
3
  // ---------------------------------------------------------------------------
4
- // WAYGRAPH_STEP=1 - human-verification overlay: one Block at a time, a
4
+ // The demo stepper (waygraph demo) - human-verification overlay: one Block at a time, a
5
5
  // browser-injected panel showing/editing that Block's MemKeys, a "Run this
6
6
  // step" gate, then a highlight ring over whatever its own verify Traits just
7
7
  // confirmed, then a "Next" gate before moving on. Ring CSS borrowed from
@@ -114,60 +114,6 @@ export const RUNNER_EXTRA_CSS =
114
114
  "#wg-zoom-badge .wg-zoom-ico svg{width:14px;height:14px;display:block;}" +
115
115
  "#wg-zoom-badge .wg-zoom-val{color:#c9a6ff;font-variant-numeric:tabular-nums;font-weight:800;min-width:3.2em;}" +
116
116
  "#wg-zoom-badge[data-zoomed=\"1\"]{border-color:#7C3AED;}" +
117
- // Network-tab overlay (real page traffic via network-capture.js + defineApiBlock's own
118
- // page.request calls) - bottom-left, the one corner free of the zoom HUD (bottom-right) and the
119
- // typing chip (bottom-center). Persistent (does not auto-fade) once any entry exists - this is a
120
- // real Postman/DevTools-style inspector meant to be reviewed, not a flash-and-gone toast.
121
- "#wg-network-log{position:fixed;z-index:2147483646;bottom:14px;left:14px;min-width:260px;" +
122
- "max-width:min(92vw,420px);max-height:min(60vh,420px);display:flex;flex-direction:column;" +
123
- "border-radius:10px;background:" + SURFACE + ";color:#f0e8ff;border:1px solid rgba(124,58,237,.45);" +
124
- "box-shadow:0 8px 22px rgba(0,0,0,.4);font:12px/1.4 ui-monospace,\"SF Mono\",Consolas,monospace;" +
125
- "opacity:0;transform:translateY(8px);transition:opacity .25s ease,transform .25s ease;overflow:hidden;}" +
126
- "#wg-network-log.wg-in{opacity:1;transform:translateY(0);}" +
127
- "#wg-network-log.wg-net-collapsed .wg-net-list{display:none;}" +
128
- "#wg-network-log .wg-net-header{display:flex;align-items:center;gap:6px;padding:8px 10px;" +
129
- "border-bottom:1px solid rgba(124,58,237,.25);cursor:default;flex:0 0 auto;}" +
130
- "#wg-network-log .wg-net-title{font:700 10px/1.2 system-ui,sans-serif;color:#c9a6ff;" +
131
- "letter-spacing:.06em;text-transform:uppercase;flex:1 1 auto;}" +
132
- "#wg-network-log .wg-net-count{font:700 10px/1.2 system-ui,sans-serif;color:#9CA3AF;" +
133
- "background:rgba(124,58,237,.22);border-radius:999px;padding:1px 7px;}" +
134
- "#wg-network-log .wg-net-collapse{flex:0 0 auto;width:18px;height:18px;border-radius:5px;border:none;" +
135
- "background:rgba(124,58,237,.28);color:#e8dcff;font:700 13px/1 system-ui,sans-serif;cursor:pointer;" +
136
- "pointer-events:auto;}" +
137
- "#wg-network-log .wg-net-collapse:hover{background:rgba(124,58,237,.42);}" +
138
- "#wg-network-log .wg-net-list{list-style:none;margin:0;padding:6px 8px;display:flex;" +
139
- "flex-direction:column;gap:2px;overflow-y:auto;pointer-events:auto;}" +
140
- "#wg-network-log .wg-net-row{display:flex;align-items:baseline;gap:7px;opacity:0;padding:3px 4px;" +
141
- "border-radius:5px;cursor:pointer;transform:translateX(-4px);transition:opacity .2s ease,transform .2s ease;}" +
142
- "#wg-network-log .wg-net-row:hover,#wg-network-log .wg-net-row.wg-net-open{background:rgba(124,58,237,.18);}" +
143
- "#wg-network-log .wg-net-row.wg-in{opacity:1;transform:translateX(0);}" +
144
- "#wg-network-log .wg-net-method{flex:0 0 auto;font-weight:800;font-size:10px;min-width:38px;}" +
145
- "#wg-network-log .wg-net-method-get{color:#3B82F6;}" +
146
- "#wg-network-log .wg-net-method-post{color:#22C55E;}" +
147
- "#wg-network-log .wg-net-method-put{color:#F97316;}" +
148
- "#wg-network-log .wg-net-method-delete{color:#EF4444;}" +
149
- "#wg-network-log .wg-net-method-other{color:#9CA3AF;}" +
150
- "#wg-network-log .wg-net-path{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;" +
151
- "white-space:nowrap;color:#e0e0f0;}" +
152
- "#wg-network-log .wg-net-status{flex:0 0 auto;font-weight:700;color:#22C55E;}" +
153
- "#wg-network-log .wg-net-row[data-ok=\"0\"] .wg-net-status{color:#EF4444;}" +
154
- "#wg-network-log .wg-net-ms{flex:0 0 auto;color:#9CA3AF;font-size:10px;}" +
155
- "#wg-network-log .wg-net-detail{list-style:none;background:rgba(0,0,0,.28);border-radius:6px;" +
156
- "padding:8px;margin:2px 0 6px;}" +
157
- "#wg-network-log .wg-net-detail-section{margin-bottom:8px;}" +
158
- "#wg-network-log .wg-net-detail-section:last-child{margin-bottom:0;}" +
159
- "#wg-network-log .wg-net-detail-section h4{margin:0 0 4px;font:700 9px/1.2 system-ui,sans-serif;" +
160
- "color:#9CA3AF;letter-spacing:.06em;text-transform:uppercase;}" +
161
- "#wg-network-log .wg-net-headers{width:100%;border-collapse:collapse;font-size:11px;}" +
162
- "#wg-network-log .wg-net-headers td{padding:1px 6px 1px 0;vertical-align:top;word-break:break-all;}" +
163
- "#wg-network-log .wg-net-headers td:first-child{color:#c9a6ff;white-space:nowrap;}" +
164
- "#wg-network-log .wg-net-body{margin:0;font-size:11px;white-space:pre-wrap;word-break:break-all;" +
165
- "max-height:160px;overflow-y:auto;color:#e0e0f0;}" +
166
- "#wg-network-log .wg-net-curl{color:#7dd3fc;}" +
167
- "#wg-network-log .wg-net-empty{font-size:11px;color:#6b7280;font-style:italic;}" +
168
- "#wg-network-log .wg-net-copy{margin-top:5px;padding:3px 9px;border-radius:5px;border:1px solid rgba(124,58,237,.5);" +
169
- "background:rgba(124,58,237,.22);color:#e8dcff;font:700 10px system-ui,sans-serif;cursor:pointer;}" +
170
- "#wg-network-log .wg-net-copy:hover{background:rgba(124,58,237,.38);}" +
171
117
  // Typing chip (method fills: username / password / etc.)
172
118
  "#wg-typing-badge{position:fixed;z-index:2147483646;bottom:88px;left:50%;transform:translateX(-50%);" +
173
119
  "display:flex;align-items:center;gap:8px;padding:8px 14px;" +
@@ -25,6 +25,9 @@ import { ignorePageGone } from "../../engine/page-gone.js";
25
25
  export async function installOverlay(page, title, ui, caption, networkTabVisible) {
26
26
  // ring/cursor/banner/dock come from the ONE shared bundle every surface installs (ui/css/*.css).
27
27
  await ensureShadowRoot(page);
28
+ // Runner-only chrome (step panel, zoom/typing/device chips) - separate key, no collision. First, so nothing below
29
+ // builds chrome ahead of its CSS (an element painted unstyled for a moment, then restyled).
30
+ await page.evaluate(([css, key]) => __wgCss(css, key), [RUNNER_EXTRA_CSS, "wg-runner-css"]).catch(ignorePageGone);
28
31
  // Idempotent (guarded on page.__wgNetworkCaptureInstalled) - safe to call every step even though
29
32
  // installOverlay itself runs every step; the page.on() listeners this installs are attached to
30
33
  // the Page object, not any one document, so they already survive navigations on their own.
@@ -46,8 +49,6 @@ export async function installOverlay(page, title, ui, caption, networkTabVisible
46
49
  if (networkTabVisible !== undefined) {
47
50
  await page.evaluate(setNetworkTabVisible, networkTabVisible).catch(ignorePageGone);
48
51
  }
49
- // Runner-only chrome (step panel, zoom/typing/device chips) - separate key, no collision.
50
- await page.evaluate(([css, key]) => __wgCss(css, key), [RUNNER_EXTRA_CSS, "wg-runner-css"]).catch(ignorePageGone);
51
52
  await page.addStyleTag({ content: HOST_CSS }).catch(ignorePageGone);
52
53
  // Default top-left; override with WAYGRAPH_TITLE_POS=left|center|right.
53
54
  // Click cycles left -> center -> right (persisted in localStorage so a
@@ -51,11 +51,3 @@ export declare function captionForLocator(page: WgPage, locator: Locator, stubs:
51
51
  }>;
52
52
  export declare function dwellMatchedStub(page: WgPage, locator: Locator, stubs: readonly ResolvedHighlight[] | undefined, pacing: Pick<Pacing, "gatesFast" | "demoPace"> | undefined): Promise<void>;
53
53
  export declare function hideRing(page: WgPage): Promise<void>;
54
- /**
55
- * True if the engine's own narrate() (waygraph's public export, for a Block
56
- * author explicitly captioning one action) JUST positioned the ring for
57
- * THIS action, moments ago - the automatic per-fill/per-click narration
58
- * below should not immediately overwrite an author's own explicit caption
59
- * with its generic guess.
60
- */
61
- export declare function wasJustNarrated(page: WgPage): Promise<boolean>;
@@ -405,18 +405,3 @@ export async function hideRing(page) {
405
405
  })
406
406
  .catch(ignorePageGone);
407
407
  }
408
- /**
409
- * True if the engine's own narrate() (waygraph's public export, for a Block
410
- * author explicitly captioning one action) JUST positioned the ring for
411
- * THIS action, moments ago - the automatic per-fill/per-click narration
412
- * below should not immediately overwrite an author's own explicit caption
413
- * with its generic guess.
414
- */
415
- export async function wasJustNarrated(page) {
416
- return page
417
- .evaluate(() => {
418
- const w = window;
419
- return typeof w.__wgLastNarrate === "number" && Date.now() - w.__wgLastNarrate < 500;
420
- })
421
- .catch(() => false);
422
- }
@@ -215,3 +215,43 @@
215
215
  background: #fff; border-radius: var(--wg-radius-lg);
216
216
  padding: 10px 18px; box-shadow: var(--wg-shadow-banner);
217
217
  }
218
+
219
+
220
+ /* The network tab: real page traffic (network-capture.ts) and defineApiBlock's own calls, bottom-left (the one corner
221
+ * free of the zoom HUD and the typing chip). Hidden until ctx.showNetworkTab(); stays up once shown. In the shared
222
+ * bundle, not the runner CSS: a navigation rebuilds the panel the moment the shadow root is back, before any later
223
+ * stylesheet, and without its CSS it painted unstyled and visible, then faded out. */
224
+ #wg-network-log{position:fixed;z-index:2147483646;bottom:14px;left:14px;min-width:260px;max-width:min(92vw,420px);max-height:min(60vh,420px);display:flex;flex-direction:column;border-radius:10px;background:var(--wg-surface);color:#f0e8ff;border:1px solid rgba(124,58,237,.45);box-shadow:0 8px 22px rgba(0,0,0,.4);font:12px/1.4 ui-monospace,"SF Mono",Consolas,monospace;opacity:0;transform:translateY(8px);transition:opacity .25s ease,transform .25s ease;overflow:hidden;}
225
+ #wg-network-log.wg-in{opacity:1;transform:translateY(0);}
226
+ #wg-network-log.wg-net-collapsed .wg-net-list{display:none;}
227
+ #wg-network-log .wg-net-header{display:flex;align-items:center;gap:6px;padding:8px 10px;border-bottom:1px solid rgba(124,58,237,.25);cursor:default;flex:0 0 auto;}
228
+ #wg-network-log .wg-net-title{font:700 10px/1.2 system-ui,sans-serif;color:#c9a6ff;letter-spacing:.06em;text-transform:uppercase;flex:1 1 auto;}
229
+ #wg-network-log .wg-net-count{font:700 10px/1.2 system-ui,sans-serif;color:#9CA3AF;background:rgba(124,58,237,.22);border-radius:999px;padding:1px 7px;}
230
+ #wg-network-log .wg-net-collapse{flex:0 0 auto;width:18px;height:18px;border-radius:5px;border:none;background:rgba(124,58,237,.28);color:#e8dcff;font:700 13px/1 system-ui,sans-serif;cursor:pointer;pointer-events:auto;}
231
+ #wg-network-log .wg-net-collapse:hover{background:rgba(124,58,237,.42);}
232
+ #wg-network-log .wg-net-list{list-style:none;margin:0;padding:6px 8px;display:flex;flex-direction:column;gap:2px;overflow-y:auto;pointer-events:auto;}
233
+ #wg-network-log .wg-net-row{display:flex;align-items:baseline;gap:7px;opacity:0;padding:3px 4px;border-radius:5px;cursor:pointer;transform:translateX(-4px);transition:opacity .2s ease,transform .2s ease;}
234
+ #wg-network-log .wg-net-row:hover,#wg-network-log .wg-net-row.wg-net-open{background:rgba(124,58,237,.18);}
235
+ #wg-network-log .wg-net-row.wg-in{opacity:1;transform:translateX(0);}
236
+ #wg-network-log .wg-net-method{flex:0 0 auto;font-weight:800;font-size:10px;min-width:38px;}
237
+ #wg-network-log .wg-net-method-get{color:#3B82F6;}
238
+ #wg-network-log .wg-net-method-post{color:#22C55E;}
239
+ #wg-network-log .wg-net-method-put{color:#F97316;}
240
+ #wg-network-log .wg-net-method-delete{color:#EF4444;}
241
+ #wg-network-log .wg-net-method-other{color:#9CA3AF;}
242
+ #wg-network-log .wg-net-path{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#e0e0f0;}
243
+ #wg-network-log .wg-net-status{flex:0 0 auto;font-weight:700;color:#22C55E;}
244
+ #wg-network-log .wg-net-row[data-ok="0"] .wg-net-status{color:#EF4444;}
245
+ #wg-network-log .wg-net-ms{flex:0 0 auto;color:#9CA3AF;font-size:10px;}
246
+ #wg-network-log .wg-net-detail{list-style:none;background:rgba(0,0,0,.28);border-radius:6px;padding:8px;margin:2px 0 6px;}
247
+ #wg-network-log .wg-net-detail-section{margin-bottom:8px;}
248
+ #wg-network-log .wg-net-detail-section:last-child{margin-bottom:0;}
249
+ #wg-network-log .wg-net-detail-section h4{margin:0 0 4px;font:700 9px/1.2 system-ui,sans-serif;color:#9CA3AF;letter-spacing:.06em;text-transform:uppercase;}
250
+ #wg-network-log .wg-net-headers{width:100%;border-collapse:collapse;font-size:11px;}
251
+ #wg-network-log .wg-net-headers td{padding:1px 6px 1px 0;vertical-align:top;word-break:break-all;}
252
+ #wg-network-log .wg-net-headers td:first-child{color:#c9a6ff;white-space:nowrap;}
253
+ #wg-network-log .wg-net-body{margin:0;font-size:11px;white-space:pre-wrap;word-break:break-all;max-height:160px;overflow-y:auto;color:#e0e0f0;}
254
+ #wg-network-log .wg-net-curl{color:#7dd3fc;}
255
+ #wg-network-log .wg-net-empty{font-size:11px;color:#6b7280;font-style:italic;}
256
+ #wg-network-log .wg-net-copy{margin-top:5px;padding:3px 9px;border-radius:5px;border:1px solid rgba(124,58,237,.5);background:rgba(124,58,237,.22);color:#e8dcff;font:700 10px system-ui,sans-serif;cursor:pointer;}
257
+ #wg-network-log .wg-net-copy:hover{background:rgba(124,58,237,.38);}
@@ -1,18 +1,19 @@
1
1
  import { envFlag } from "../../engine/env.js";
2
- import { hasAuthoredStubAfter, resolveSlides, runStubPhase } from "../narration/narration.js";
2
+ import { hasAuthoredStubAfter, runStubPhase } from "../narration/narration.js";
3
3
  import { applyHighlightStyleDefaults, formatHighlightCaption } from "../narration/style.js";
4
4
  import { logBannerDom, probeBanner } from "./banner-log.js";
5
5
  import { logDocks } from "./before.js";
6
6
  import { demoLog, logStubPhaseFixtures, logTodoDockFull, paceSpeakFields, summarizeDevice } from "./demo-log.js";
7
7
  import { renderAfterStep } from "./panel-after.js";
8
- import { presentSlides } from "./slides.js";
8
+ import { showSlides } from "./slides.js";
9
9
  import { extractVerifyHighlights } from "./step-panels.js";
10
10
  import { forceCollapsed } from "./view.js";
11
11
  const quiet = () => envFlag("WAYGRAPH_JSON");
12
12
  export async function showAfter(ctx, view, result) {
13
13
  const { stage, refs } = ctx;
14
- await showSlides(ctx, view, result);
15
14
  const after = hasAuthoredStubAfter(view.step.block, result) ? await narrateAfter(ctx, view, result) : undefined;
15
+ if (after)
16
+ await showSlides(ctx, view, after.phase.slides);
16
17
  const applied = after ? await stage.applyPhase(after.phase) : { appliedTodo: { sync: "keep" }, appliedDevice: { sync: "keep" } };
17
18
  await renderAfterStep(ctx.page, {
18
19
  index: view.index,
@@ -62,20 +63,6 @@ export async function showAfter(ctx, view, result) {
62
63
  }
63
64
  await ctx.gate.wait();
64
65
  }
65
- async function showSlides(ctx, view, result) {
66
- const slides = resolveSlides(view.step.block, { out: result }).map((s) => applyHighlightStyleDefaults(s, view.step.highlightStyle));
67
- if (slides.length === 0)
68
- return;
69
- await presentSlides(ctx.page, slides, {
70
- title: ctx.title,
71
- blockName: view.step.block.name,
72
- fast: ctx.pacing.gatesFast,
73
- pace: ctx.pacing.demoPace,
74
- episodeNumber: view.step.episodeNumber,
75
- episodeTitle: view.step.episodeTitle,
76
- autoplayMs: ctx.gate.autoplayMs,
77
- });
78
- }
79
66
  /** Runs the Block's stubAfter; its rings carry the flow's highlight style and a caption. */
80
67
  async function narrateAfter(ctx, view, result) {
81
68
  const style = view.step.highlightStyle;
@@ -1,4 +1,4 @@
1
- // Before a demo step runs: play the Block's stubBefore narration (rings, todos, device, title, caption) and show the
1
+ // Before a demo step runs: play the Block's stubBefore narration (slides, rings, todos, device, title, caption) and show the
2
2
  // step's panel, then log what was painted so an agent without eyes can check it.
3
3
  import { envFlag } from "../../engine/env.js";
4
4
  import { runStubPhase } from "../narration/narration.js";
@@ -6,6 +6,7 @@ import { applyHighlightStyleDefaults, formatHighlightCaption } from "../narratio
6
6
  import { logBannerDom, probeBanner } from "./banner-log.js";
7
7
  import { demoLog, logStubPhaseFixtures, logTodoDockFull, paceSpeakFields, summarizeDevice } from "./demo-log.js";
8
8
  import { renderBeforeStep } from "./panel-before.js";
9
+ import { showSlides } from "./slides.js";
9
10
  import { probeTodoDocksOnPage } from "./todo-advance.js";
10
11
  import { forceCollapsed } from "./view.js";
11
12
  const quiet = () => envFlag("WAYGRAPH_JSON");
@@ -13,6 +14,7 @@ export async function showBefore(ctx, view) {
13
14
  const { stage } = ctx;
14
15
  const phase = await narrateBefore(ctx, view);
15
16
  const { appliedTodo } = await stage.applyPhase(phase, { reapplyDeviceOnKeep: true });
17
+ await showSlides(ctx, view, phase.slides);
16
18
  if (phase.title && String(phase.title).trim())
17
19
  ctx.stickyTitle = String(phase.title).trim();
18
20
  view.overlayTitle = ctx.stickyTitle || (view.step.episodeTitle && String(view.step.episodeTitle).trim()) || ctx.title || "waygraph demo";
@@ -208,7 +208,7 @@ export function logStubPhaseFixtures(phaseName, phase) {
208
208
  : phase.todoSync);
209
209
  }
210
210
  phase.highlights.forEach((h, i) => {
211
- demoFixture(tag, "ctx.ring/highlights[" + i + "]", {
211
+ demoFixture(tag, "ctx.ring[" + i + "]", {
212
212
  selector: h.selector,
213
213
  label: h.label,
214
214
  detail: h.detail,
@@ -1,5 +1,5 @@
1
1
  import { installOverlay } from "../overlay/overlay.js";
2
- import { applyHighlightZoom, captionForLocator, dwellMatchedStub, ensureLocatorInView, hideRing, matchStubIndexForLocator, showRing, wasJustNarrated } from "../overlay/rings.js";
2
+ import { applyHighlightZoom, captionForLocator, dwellMatchedStub, ensureLocatorInView, hideRing, matchStubIndexForLocator, showRing } from "../overlay/rings.js";
3
3
  import { syncTodoDockAdvance } from "./todo-advance.js";
4
4
  import { clickPulseAt, moveCursorTo } from "../overlay/cursor.js";
5
5
  import { demoHighlight } from "./demo-log.js";
@@ -109,7 +109,7 @@ export function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stu
109
109
  catch {
110
110
  /* ignore */
111
111
  }
112
- if (box && !(await wasJustNarrated(page))) {
112
+ if (box) {
113
113
  // Only trust the "last mem.get()" as THIS fill's source if it
114
114
  // happened recently - a stale read from several actions ago is
115
115
  // more likely unrelated than actually describing this field.
@@ -148,11 +148,6 @@ export function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stu
148
148
  if (cap && cap.label)
149
149
  typeLabel = String(cap.label).slice(0, 40);
150
150
  }
151
- else if (box) {
152
- // Ring/caption already handled by narrate() - still move the
153
- // cursor there, just skip re-showing the ring with a generic label.
154
- await moveCursorTo(page, box, cursorMs(200));
155
- }
156
151
  await page
157
152
  .evaluate((label) => {
158
153
  if (window.__wgSetTypingBadge)
@@ -191,8 +186,7 @@ export function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stu
191
186
  // own click scroll is instant and reads as a teleport under --fast.
192
187
  await ensureLocatorInView(locator, { instant: skipTheater() });
193
188
  const box = await locator.boundingBox();
194
- const narrated = box ? await wasJustNarrated(page) : false;
195
- if (box && !narrated) {
189
+ if (box) {
196
190
  let fallback = "click";
197
191
  try {
198
192
  const text = (await locator.textContent())?.trim();
@@ -241,12 +235,6 @@ export function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stu
241
235
  // services/help-center-clip-engine's video-pipeline.
242
236
  await new Promise((res) => setTimeout(res, clickPrePop()));
243
237
  }
244
- else if (box) {
245
- // Ring/caption already handled by narrate() - still move the
246
- // cursor + pulse the click point, just skip re-showing the ring.
247
- clickPoint = await moveCursorTo(page, box, cursorMs(200));
248
- clickTone = "planned";
249
- }
250
238
  if (clickPoint) {
251
239
  await clickPulseAt(page, clickPoint.x, clickPoint.y, clickTone);
252
240
  // clickPulseAt only triggers the CSS animation class - it doesn't
@@ -21,7 +21,7 @@ export async function renderAfterStep(page, info) {
21
21
  // Cycle through EVERY declared/recovered highlight in order, each shown
22
22
  // long enough to actually register - "it highlights something [...] then
23
23
  // it highlights something [else] and next," not just the first one.
24
- // duration / fastMode on stubAfter (or flow fixtures) override the default
24
+ // duration / fastMode on stubAfter rings override the default
25
25
  // 900ms / 200ms holds when set.
26
26
  await cycleHighlightRings(page, highlights, gatesFast, {
27
27
  pace: info.pace,
@@ -1,6 +1,7 @@
1
1
  import type { WgPage } from "../types.js";
2
2
  import { type WaygraphSlide } from "../../engine/stub/ring.js";
3
3
  import type { DemoPace } from "../../engine/stub/pace.js";
4
+ import type { StepperContext, StepView } from "./view.js";
4
5
  /** Where the slides sit (title, Block, episode) and how fast they advance. */
5
6
  export interface SlidesOptions {
6
7
  title?: string | undefined;
@@ -21,3 +22,5 @@ export interface SlidesOptions {
21
22
  * doing nothing. In-page data-wg-acked + autoplay poll is the source of truth.
22
23
  */
23
24
  export declare function presentSlides(page: WgPage, slides: readonly WaygraphSlide[], opts?: SlidesOptions): Promise<void>;
25
+ /** Plays a stub phase's `ctx.slides(...)` for the step on screen, in the flow's highlight style. */
26
+ export declare function showSlides(ctx: StepperContext, view: StepView, slides: readonly WaygraphSlide[]): Promise<void>;