waygraph 0.15.32 → 0.15.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/engine/core.js +5 -0
  2. package/dist/engine/engine-class.d.ts +37 -0
  3. package/dist/engine/engine-class.js +98 -0
  4. package/dist/highlights/run-phase.js +2 -0
  5. package/dist/highlights/stub-ctx.js +36 -2
  6. package/dist/highlights/types.d.ts +35 -3
  7. package/dist/index.d.ts +1 -1
  8. package/dist/pilot-overlay/fixtures.d.ts +9 -1
  9. package/dist/pilot-overlay/fixtures.js +38 -1
  10. package/dist/pilot-overlay/overlay-fixtures.d.ts +9 -0
  11. package/dist/pilot-overlay/overlay-fixtures.js +45 -0
  12. package/dist/pilot-overlay/shell.js +4 -23
  13. package/dist/pilot-overlay.d.ts +1 -0
  14. package/dist/pilot-overlay.js +1 -0
  15. package/dist/runner/banner-log.js +62 -0
  16. package/dist/runner/demo-log.js +9 -1
  17. package/dist/runner/inpage/banner.js +56 -1
  18. package/dist/runner/inpage/core.js +2 -0
  19. package/dist/runner/overlay-css.js +6 -27
  20. package/dist/runner/overlay-install.js +7 -4
  21. package/dist/runner/overlay-present.d.ts +8 -0
  22. package/dist/runner/overlay-present.js +57 -0
  23. package/dist/runner/overlay-stage.js +105 -0
  24. package/dist/runner/step-mode.js +35 -126
  25. package/dist/runner/step-panels.js +3 -3
  26. package/dist/step-overlay.js +1 -3
  27. package/dist/ui/overlay.css +209 -0
  28. package/dist/ui/shadow.d.ts +7 -2
  29. package/dist/ui/shadow.js +12 -4
  30. package/dist/ui/stylesheet.d.ts +7 -0
  31. package/dist/ui/stylesheet.js +18 -0
  32. package/dist/verbose-log.d.ts +1 -0
  33. package/dist/verbose-log.js +16 -0
  34. package/examples/saucedemo/.waygraph-auto/03df352d.log +0 -0
  35. package/examples/saucedemo/.waygraph-auto/0a8c3719.log +0 -0
  36. package/examples/saucedemo/.waygraph-auto/102c38ca.log +0 -0
  37. package/examples/saucedemo/.waygraph-auto/232a1d81.log +0 -0
  38. package/examples/saucedemo/.waygraph-auto/2553d107.log +0 -0
  39. package/examples/saucedemo/.waygraph-auto/2f4f531e.log +0 -0
  40. package/examples/saucedemo/.waygraph-auto/2fdbd916.log +0 -0
  41. package/examples/saucedemo/.waygraph-auto/331c6e8e.log +0 -0
  42. package/examples/saucedemo/.waygraph-auto/38f6799d.log +0 -0
  43. package/examples/saucedemo/.waygraph-auto/3bdb2f5a.log +0 -0
  44. package/examples/saucedemo/.waygraph-auto/5b838485.log +0 -0
  45. package/examples/saucedemo/.waygraph-auto/5d13bdde.log +0 -0
  46. package/examples/saucedemo/.waygraph-auto/63b0f4cd.log +0 -0
  47. package/examples/saucedemo/.waygraph-auto/6f05bdc1.log +0 -0
  48. package/examples/saucedemo/.waygraph-auto/70279fc3.log +0 -0
  49. package/examples/saucedemo/.waygraph-auto/731a14bd.log +0 -0
  50. package/examples/saucedemo/.waygraph-auto/76128a2d.log +0 -0
  51. package/examples/saucedemo/.waygraph-auto/796ab578.log +0 -0
  52. package/examples/saucedemo/.waygraph-auto/81257bf0.log +0 -0
  53. package/examples/saucedemo/.waygraph-auto/859a261d.log +0 -0
  54. package/examples/saucedemo/.waygraph-auto/a1ab5d29.log +0 -0
  55. package/examples/saucedemo/.waygraph-auto/a4b36275.log +0 -0
  56. package/examples/saucedemo/.waygraph-auto/a94738b7.log +0 -0
  57. package/examples/saucedemo/.waygraph-auto/b3d172f3.log +0 -0
  58. package/examples/saucedemo/.waygraph-auto/b5569336.log +0 -0
  59. package/examples/saucedemo/.waygraph-auto/b57921c5.log +0 -0
  60. package/examples/saucedemo/.waygraph-auto/b9ab0983.log +0 -0
  61. package/examples/saucedemo/.waygraph-auto/d37719a6.log +0 -0
  62. package/examples/saucedemo/.waygraph-auto/da0a06bf.log +0 -0
  63. package/examples/saucedemo/.waygraph-auto/dc58eb72.log +0 -0
  64. package/examples/saucedemo/.waygraph-auto/ff616932.log +0 -0
  65. package/package.json +3 -2
  66. package/examples/saucedemo/.waygraph-auto/535e8922.json +0 -8
@@ -45,6 +45,11 @@ export function branch(block, routes) {
45
45
  },
46
46
  routes: routesAsData,
47
47
  });
48
+ // Without this, branch()'s output carried no __waygraphKind/__waygraphSalt at all (defineBlock
49
+ // never stamps them - factories do, after defineBlock returns), so a branched Block could never
50
+ // pass MapBuilder's assertMapKind/assertMapSalt checks. Real bug: branching was unusable from the
51
+ // Waygraph Map, only from a raw runGraph() call.
52
+ copyWaygraphRuntime(block, routed);
48
53
  return routed;
49
54
  }
50
55
  /**
@@ -269,6 +269,43 @@ export declare class MapBuilder<Out extends Checkpoint<string>> {
269
269
  method<NextOut extends Checkpoint<string>>(block: (MethodBlock<Out, NextOut> | EffectBlock<Out, NextOut>) & {
270
270
  name: string;
271
271
  }): MapBuilder<NextOut>;
272
+ /**
273
+ * Branch: run everything added so far, then continue into whichever `routes[tag]` matches the
274
+ * resolved Checkpoint's tag - same page, same context, same mem. `routes` must cover every tag
275
+ * of `Out` (a missing one is a compile error, matching {@link branch}'s own exhaustiveness);
276
+ * `null` means that tag is terminal (the branched Flow just returns that Checkpoint).
277
+ *
278
+ * Each non-null route is a function, not a ready-made `Flow` - a fresh Flow always starts at the
279
+ * special `S` (start) Checkpoint, but a branch's blocks start from the TAG it routes on (e.g.
280
+ * `LoggedIn`), so the function is handed a brand-new `MapBuilder` already seeded at that exact
281
+ * Checkpoint to chain `.method()/.gotoPage()/.assert()/.end()` (or another `.branch()`) off of.
282
+ *
283
+ * Real gap this fixes: the plain `branch()` helper's `.next` routing is only ever consulted by
284
+ * `runGraph`'s own graph walk - `defineFlow`/`.end()` compose steps pairwise via `connect()`,
285
+ * which never looks at `.next` at all, so a Map-built Flow had no way to branch. This method
286
+ * branches at the Flow level instead: it finalizes the prefix, runs it, and picks the next Flow
287
+ * itself once the real tag is known - no engine-level routing involved.
288
+ *
289
+ * Only the `run(context, mem[, options])` signature is supported - the page must survive across
290
+ * the branch, and the bare `run(mem, config)` convenience form always closes its own browser
291
+ * before a tag is even known, so it throws instead of silently reopening a fresh (state-losing) page.
292
+ *
293
+ * The route's first Block still sees `input.__state === "__start__"` in `act()`, not the tag that
294
+ * routed there - `runGraph` seeds every Flow's entry Block that way unconditionally, the same as
295
+ * any standalone `defineFlow([start, block, end])` already does. Read `page`/`mem`, not `input`,
296
+ * in `act()` - every real Block in this codebase already follows that convention.
297
+ *
298
+ * @example
299
+ * map({ homeOrigin }).gotoPage(NavCartBlock).branch({
300
+ * ItemInCart: (m) => m.method(RemoveFromCartBlock).end(),
301
+ * LoggedIn: (m) => m.method(AddToCartBlock).end(),
302
+ * });
303
+ */
304
+ branch<Routes extends {
305
+ [K in Out["__state"]]: ((m: MapBuilder<Extract<Out, Checkpoint<K>>>) => Flow<any>) | null;
306
+ }>(routes: Routes): Flow<{
307
+ [K in keyof Routes & string]: Routes[K] extends (m: any) => Flow<infer R> ? R : Extract<Out, Checkpoint<K>>;
308
+ }[keyof Routes & string]>;
272
309
  /**
273
310
  * Finalizes this chain into a real, runnable {@link Flow} - same object
274
311
  * `defineFlow` returns, so `withBlockVerify`/`modBlockVerify` and every
@@ -1,5 +1,6 @@
1
1
  import { end, start } from "./run-graph.js";
2
2
  import { buildFlow } from "./flow.js";
3
+ import { MemPage } from "../mem-page.js";
3
4
  import { fastForwardComposeBlock } from "./compose.js";
4
5
  /**
5
6
  * Holds engine-level configuration shared across every flow defined from it -
@@ -104,6 +105,59 @@ function assertMapOrigin(block, homeOrigin, expect, method) {
104
105
  `home origin (${wantOrigin}) - use .gotoPage() for an internal destination.`);
105
106
  }
106
107
  }
108
+ /**
109
+ * Builds the `Flow` object `MapBuilder.branch()` returns: run `before`, then dispatch on the
110
+ * resolved tag into whichever `routes` entry matches (same page/context/mem), or return that
111
+ * Checkpoint as-is when its route is `null`. Factored out so `withBlockVerify`/`modBlockVerify`
112
+ * can patch `before` and rebuild the same branched shape, exactly like a plain Flow's own.
113
+ */
114
+ function buildBranchedFlow(before, routes) {
115
+ const run = (async (contextOrMem, memOrConfig, options) => {
116
+ if (contextOrMem instanceof MemPage) {
117
+ throw new Error("Waygraph map: a .branch()-ed Flow can't run via run(mem, config) - that convenience form " +
118
+ "always closes its own browser/page before a branch's tag is even known. Call " +
119
+ "run(context, mem[, options]) instead, keeping the same page across the branch.");
120
+ }
121
+ const context = contextOrMem;
122
+ const mem = memOrConfig;
123
+ const gotOwnPage = options?.page === undefined;
124
+ const page = options?.page ?? (await context.newPage());
125
+ const closeOnFinish = options?.closeOnFinish ?? gotOwnPage;
126
+ const first = (await before.run(context, mem, { ...options, page, closeOnFinish: false }));
127
+ const branchFlow = routes[first.result.__state];
128
+ if (!branchFlow) {
129
+ if (options?.closeOnFinish === false)
130
+ return { result: first.result, page: first.page };
131
+ if (closeOnFinish)
132
+ await first.page.close();
133
+ return first.result;
134
+ }
135
+ return branchFlow.run(context, mem, { ...options, page: first.page, closeOnFinish });
136
+ });
137
+ return {
138
+ resetSession: before.resetSession,
139
+ ...(before.title !== undefined ? { title: before.title } : {}),
140
+ ...(before.expectedFailureReason !== undefined ? { expectedFailureReason: before.expectedFailureReason } : {}),
141
+ ...(before.highlightFixtures !== undefined ? { highlightFixtures: before.highlightFixtures } : {}),
142
+ ...(before.demoPace !== undefined ? { demoPace: before.demoPace } : {}),
143
+ ...(before.highlightStyle !== undefined ? { highlightStyle: before.highlightStyle } : {}),
144
+ ...(before.memStub !== undefined ? { memStub: before.memStub } : {}),
145
+ // Static introspection (`waygraph graph`/list): the fixed prefix, plus every branch's own
146
+ // Blocks prefixed by the tag that leads to them - can't know at analysis time which one a real
147
+ // run takes, so this shows all of them rather than none.
148
+ blocks: () => [
149
+ ...before.blocks(),
150
+ ...Object.entries(routes).flatMap(([tag, f]) => f ? f.blocks().map((b) => ({ ...b, name: `${tag} -> ${b.name}` })) : []),
151
+ ],
152
+ run,
153
+ withBlockVerify(block, verify) {
154
+ return buildBranchedFlow(before.withBlockVerify(block, verify), routes);
155
+ },
156
+ modBlockVerify(block, nameOrIndex, newCheck) {
157
+ return buildBranchedFlow(before.modBlockVerify(block, nameOrIndex, newCheck), routes);
158
+ },
159
+ };
160
+ }
107
161
  /**
108
162
  * Fluent builder over {@link Engine.defineFlow} - see `map()`'s own doc
109
163
  * comment for why this exists. Each step method is scoped to exactly the
@@ -226,6 +280,50 @@ export class MapBuilder {
226
280
  assertMapSalt(block, ["method", "effect"], "method");
227
281
  return this.appendStep(block);
228
282
  }
283
+ /**
284
+ * Branch: run everything added so far, then continue into whichever `routes[tag]` matches the
285
+ * resolved Checkpoint's tag - same page, same context, same mem. `routes` must cover every tag
286
+ * of `Out` (a missing one is a compile error, matching {@link branch}'s own exhaustiveness);
287
+ * `null` means that tag is terminal (the branched Flow just returns that Checkpoint).
288
+ *
289
+ * Each non-null route is a function, not a ready-made `Flow` - a fresh Flow always starts at the
290
+ * special `S` (start) Checkpoint, but a branch's blocks start from the TAG it routes on (e.g.
291
+ * `LoggedIn`), so the function is handed a brand-new `MapBuilder` already seeded at that exact
292
+ * Checkpoint to chain `.method()/.gotoPage()/.assert()/.end()` (or another `.branch()`) off of.
293
+ *
294
+ * Real gap this fixes: the plain `branch()` helper's `.next` routing is only ever consulted by
295
+ * `runGraph`'s own graph walk - `defineFlow`/`.end()` compose steps pairwise via `connect()`,
296
+ * which never looks at `.next` at all, so a Map-built Flow had no way to branch. This method
297
+ * branches at the Flow level instead: it finalizes the prefix, runs it, and picks the next Flow
298
+ * itself once the real tag is known - no engine-level routing involved.
299
+ *
300
+ * Only the `run(context, mem[, options])` signature is supported - the page must survive across
301
+ * the branch, and the bare `run(mem, config)` convenience form always closes its own browser
302
+ * before a tag is even known, so it throws instead of silently reopening a fresh (state-losing) page.
303
+ *
304
+ * The route's first Block still sees `input.__state === "__start__"` in `act()`, not the tag that
305
+ * routed there - `runGraph` seeds every Flow's entry Block that way unconditionally, the same as
306
+ * any standalone `defineFlow([start, block, end])` already does. Read `page`/`mem`, not `input`,
307
+ * in `act()` - every real Block in this codebase already follows that convention.
308
+ *
309
+ * @example
310
+ * map({ homeOrigin }).gotoPage(NavCartBlock).branch({
311
+ * ItemInCart: (m) => m.method(RemoveFromCartBlock).end(),
312
+ * LoggedIn: (m) => m.method(AddToCartBlock).end(),
313
+ * });
314
+ */
315
+ branch(routes) {
316
+ if (this.steps.length === 0) {
317
+ throw new Error("Waygraph map: .branch() needs at least one prior step - nothing to branch from. " +
318
+ "Add .gotoPage()/.gotoExternal()/.method()/.assert() first.");
319
+ }
320
+ const before = this.end();
321
+ const resolvedRoutes = {};
322
+ for (const [tag, fn] of Object.entries(routes)) {
323
+ resolvedRoutes[tag] = fn ? fn(new MapBuilder(this.engine, [], this.homeOrigin, null)) : null;
324
+ }
325
+ return buildBranchedFlow(before, resolvedRoutes);
326
+ }
229
327
  /**
230
328
  * Finalizes this chain into a real, runnable {@link Flow} - same object
231
329
  * `defineFlow` returns, so `withBlockVerify`/`modBlockVerify` and every
@@ -107,6 +107,8 @@ export async function runStubPhase(block, phase, opts) {
107
107
  ...(bag.zoom !== undefined ? { zoom: bag.zoom } : {}),
108
108
  ...(bag.zoomOut !== undefined ? { zoomOut: bag.zoomOut } : {}),
109
109
  ...(bag.title !== undefined && bag.title !== "" ? { title: bag.title } : {}),
110
+ ...(bag.titlePos !== undefined ? { titlePos: bag.titlePos } : {}),
111
+ ...(bag.bannerUi !== undefined ? { bannerUi: bag.bannerUi } : {}),
110
112
  ...(bag.todoPos !== undefined ? { todoPos: bag.todoPos } : {}),
111
113
  ...(bag.todoDockUi !== undefined ? { todoDockUi: bag.todoDockUi } : {}),
112
114
  ...(bag.todoParallel === true ? { todoParallel: true } : {}),
@@ -162,11 +162,35 @@ export function createStubCtx(bag, opts) {
162
162
  zoomOut(keep) {
163
163
  bag.zoomOut = !!keep;
164
164
  },
165
- title(text) {
165
+ title(text, opts) {
166
166
  bag.title = String(text ?? "");
167
+ const p = normalizeBannerPos(opts?.pos);
168
+ if (p)
169
+ bag.titlePos = p;
167
170
  },
168
- banner(text) {
171
+ banner(text, opts) {
169
172
  bag.title = String(text ?? "");
173
+ const p = normalizeBannerPos(opts?.pos);
174
+ if (p)
175
+ bag.titlePos = p;
176
+ },
177
+ titlePos(side) {
178
+ const p = normalizeBannerPos(side);
179
+ if (p)
180
+ bag.titlePos = p;
181
+ },
182
+ bannerPos(side) {
183
+ const p = normalizeBannerPos(side);
184
+ if (p)
185
+ bag.titlePos = p;
186
+ },
187
+ bannerUi(opts) {
188
+ if (!opts || typeof opts !== "object")
189
+ return;
190
+ bag.bannerUi = { ...(bag.bannerUi || {}), ...opts };
191
+ },
192
+ hideBanner() {
193
+ bag.bannerUi = { ...(bag.bannerUi || {}), hidden: true };
170
194
  },
171
195
  todoPos(side) {
172
196
  const p = normalizeTodoPos(side);
@@ -256,6 +280,12 @@ export function createStubCtx(bag, opts) {
256
280
  bag.zoomOut = !!partial.zoomOut;
257
281
  if (partial.title !== undefined)
258
282
  bag.title = String(partial.title ?? "");
283
+ const bp = normalizeBannerPos(partial.titlePos);
284
+ if (bp)
285
+ bag.titlePos = bp;
286
+ if (partial.bannerUi && typeof partial.bannerUi === "object") {
287
+ bag.bannerUi = { ...(bag.bannerUi || {}), ...partial.bannerUi };
288
+ }
259
289
  const tp = normalizeTodoPos(partial.todoPos);
260
290
  if (tp)
261
291
  bag.todoPos = tp;
@@ -279,3 +309,7 @@ export function liftLegacySlotTodos(bag) {
279
309
  }
280
310
  }
281
311
  }
312
+ function normalizeBannerPos(raw) {
313
+ const v = String(raw ?? "").toLowerCase();
314
+ return v === "left" || v === "center" || v === "right" ? v : undefined;
315
+ }
@@ -41,6 +41,15 @@ export type WaygraphTodoInput = string | WaygraphTodoItem;
41
41
  * - `bullets` - flat bullet list (no arrow/checkbox; open notes / trimmed dock)
42
42
  */
43
43
  export type TodoListStyle = "sequential" | "checklist" | "bullets";
44
+ /** Where the top banner sits. */
45
+ export type BannerPos = "left" | "center" | "right";
46
+ /** Banner UX, mirrors {@link TodoDockUiOpts}: pass `false` fields to opt out for this phase. */
47
+ export interface BannerUiOpts {
48
+ /** Move to a free spot while a highlight ring would overlap it (default true). */
49
+ collision?: boolean;
50
+ /** Hide the banner entirely for this phase. */
51
+ hidden?: boolean;
52
+ }
44
53
  /** One titled group in the floating todo dock (FR / Scenarios / ACs, etc.). */
45
54
  export type WaygraphTodoGroupInput = {
46
55
  /** Stable group id -> `data-wg-todo-group`. */
@@ -146,6 +155,10 @@ export type StubPhaseFixtures = {
146
155
  zoomOut?: boolean;
147
156
  /** Top demo banner title (the purple "waygraph demo" card). */
148
157
  title?: string;
158
+ /** Banner side/position (`left` | `center` | `right`). Authored control, like {@link todoPos}. */
159
+ titlePos?: BannerPos;
160
+ /** Banner UX: collision (move away from a ring it overlaps), hidden. See {@link BannerUiOpts}. */
161
+ bannerUi?: BannerUiOpts;
149
162
  /**
150
163
  * Floating checklist dock side. `left` | `right`.
151
164
  * Authored control (also click / WAYGRAPH_TODO_POS / --todo-left|right).
@@ -373,10 +386,25 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
373
386
  * Default zoomOut for rings in this phase. `false` = keep camera between rings.
374
387
  */
375
388
  zoomOut(keep: boolean): void;
376
- /** Top banner title (waygraph demo card). */
377
- title(text: string): void;
389
+ /** Top banner title (waygraph demo card). `opts.pos` also places it (left | center | right). */
390
+ title(text: string, opts?: {
391
+ pos?: BannerPos;
392
+ }): void;
378
393
  /** Alias for {@link StubCtx.title}. */
379
- banner(text: string): void;
394
+ banner(text: string, opts?: {
395
+ pos?: BannerPos;
396
+ }): void;
397
+ /** Place the banner: `left` | `center` | `right` (authored; click / env are fallbacks). */
398
+ titlePos(side: BannerPos): void;
399
+ /** Alias for {@link StubCtx.titlePos}. */
400
+ bannerPos(side: BannerPos): void;
401
+ /**
402
+ * Banner UX: `{ collision: false }` keeps it put even over a ring; `{ hidden: true }` hides it.
403
+ * @example ctx.bannerUi({ collision: false });
404
+ */
405
+ bannerUi(opts: BannerUiOpts): void;
406
+ /** Shorthand for `bannerUi({ hidden: true })`. */
407
+ hideBanner(): void;
380
408
  /**
381
409
  * Floating checklist dock side (`left` | `right`). Authors control this in
382
410
  * code; click / env / --todo-left|right are fallbacks.
@@ -433,6 +461,8 @@ export type StubPhaseResult = {
433
461
  zoom?: number;
434
462
  zoomOut?: boolean;
435
463
  title?: string;
464
+ titlePos?: BannerPos;
465
+ bannerUi?: BannerUiOpts;
436
466
  todoPos?: "left" | "right";
437
467
  /** When true, keep other docks (multi-todo). Default false = replace. */
438
468
  todoParallel?: boolean;
@@ -528,6 +558,8 @@ export type StubBagState = {
528
558
  zoom?: number | undefined;
529
559
  zoomOut?: boolean | undefined;
530
560
  title?: string | undefined;
561
+ titlePos?: BannerPos | undefined;
562
+ bannerUi?: BannerUiOpts | undefined;
531
563
  todoPos?: "left" | "right" | undefined;
532
564
  /** Author patched todo-dock UX this phase. */
533
565
  todoDockUi?: TodoDockUiOpts | undefined;
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { registerMemStub, seedMemStub, getMemStub } from "./mem-stub.js";
5
5
  export { Trait, urlMatches, textEquals, visible, disabled, enabled, frameVisible, frameTextEquals, frameContainsText, } from "./trait.js";
6
6
  export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withMemStub, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, chainFlow, defineLayout, map, MapBuilder, } from "./engine.js";
7
7
  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";
8
- export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, WaygraphTodoItem, WaygraphTodoInput, WaygraphTodoGroup, WaygraphTodoGroupInput, WaygraphTodosOpts, TodoDockUiOpts, 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";
8
+ 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";
9
9
  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 "./engine.js";
10
10
  export type { ActionPage } from "./types.js";
11
11
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
@@ -1,4 +1,4 @@
1
- import type { DevicePreset, DeviceState, HighlightTone, TodoDockUiOpts } from "../highlights.js";
1
+ import type { BannerPos, BannerUiOpts, DevicePreset, DeviceState, HighlightTone, TodoDockUiOpts } from "../highlights.js";
2
2
  import type { Page } from "@playwright/test";
3
3
  /** One ring the Pilot agent wants painted (same fields as demo stub rings). */
4
4
  export type PilotFixtureRing = {
@@ -68,6 +68,14 @@ export type PilotHighlightFixtures = {
68
68
  device?: DevicePreset | DeviceState;
69
69
  /** Drop every agent fixture ring/todo/focus/zoom immediately. */
70
70
  clear?: boolean;
71
+ /** Top banner title card (same as `ctx.title`). */
72
+ title?: string;
73
+ /** Banner position (same as `ctx.titlePos`). */
74
+ titlePos?: BannerPos;
75
+ /** Banner UX: collision / hidden (same as `ctx.bannerUi`). */
76
+ bannerUi?: BannerUiOpts;
77
+ /** Alias of {@link todoUi} - the name `ctx.todoDockUi` uses. */
78
+ todoDockUi?: TodoDockUiOpts;
71
79
  };
72
80
  /**
73
81
  * Paint (or clear) agent-authored highlight fixtures on the live page.
@@ -92,8 +92,41 @@ export async function showPilotFixtures(page, fixtures) {
92
92
  : typeof fixtures.device === "string"
93
93
  ? fixtures.device
94
94
  : fixtures.device.preset || "device";
95
+ const bannerTitle = fixtures.clear ? undefined : fixtures.title;
96
+ const bannerPos = fixtures.clear ? undefined : fixtures.titlePos;
97
+ const bannerHidden = !!fixtures.clear || !!fixtures.bannerUi?.hidden;
98
+ const bannerCollision = fixtures.bannerUi?.collision !== false;
95
99
  return page
96
- .evaluate(({ rings, todos, todoIndex, todoTitle, todoPos, holdMs, zoom, zoomSelector, zoomOut, clearAll, deviceLabel, todoUi, }) => {
100
+ .evaluate(({ rings, todos, todoIndex, todoTitle, todoPos, holdMs, zoom, zoomSelector, zoomOut, clearAll, deviceLabel, todoUi, bannerTitle, bannerPos, bannerHidden, bannerCollision, }) => {
101
+ // Same #wg-banner element/CSS the demo runner paints (ui/css/banner.css, already in the
102
+ // shared bundle) - so ctx.title / ctx.titlePos / ctx.bannerUi behave identically on a Pilot
103
+ // session, not a separate re-implementation.
104
+ if (bannerHidden) {
105
+ const b = __wgById("wg-banner");
106
+ if (b)
107
+ b.style.display = "none";
108
+ }
109
+ else if (bannerTitle) {
110
+ let banner = __wgById("wg-banner");
111
+ if (!banner) {
112
+ banner = document.createElement("div");
113
+ banner.id = "wg-banner";
114
+ const tag = document.createElement("span");
115
+ tag.className = "wg-banner-tag";
116
+ tag.textContent = "waygraph pilot";
117
+ const text = document.createElement("span");
118
+ text.className = "wg-banner-text";
119
+ banner.appendChild(tag);
120
+ banner.appendChild(text);
121
+ __wgAdd(banner);
122
+ }
123
+ banner.style.display = "";
124
+ banner.dataset.pos = bannerPos || banner.dataset.pos || "left";
125
+ banner.dataset.collision = bannerCollision ? "1" : "0";
126
+ const textEl = banner.querySelector(".wg-banner-text");
127
+ if (textEl)
128
+ textEl.textContent = bannerTitle;
129
+ }
97
130
  const w = window;
98
131
  if (w.__wgPilotFxHideTimer)
99
132
  clearTimeout(w.__wgPilotFxHideTimer);
@@ -329,6 +362,10 @@ export async function showPilotFixtures(page, fixtures) {
329
362
  clearAll: fixtures.clear === true,
330
363
  deviceLabel,
331
364
  todoUi,
365
+ bannerTitle,
366
+ bannerPos,
367
+ bannerHidden,
368
+ bannerCollision,
332
369
  })
333
370
  .catch(() => ({ painted: 0, missing: rings.map((r) => r.selector) }));
334
371
  }
@@ -0,0 +1,9 @@
1
+ import type { StubPhaseResult } from "../highlights.js";
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>;
@@ -0,0 +1,45 @@
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
+ }
@@ -1,8 +1,6 @@
1
1
  // Split out of the former 950-line pilot-overlay.ts (see src/ARCHITECTURE.md). Behavior unchanged.
2
2
  import { SURFACE } from "../ui/tokens.js";
3
3
  import { ensureShadowRoot, installShadowRoot } from "../ui/shadow.js";
4
- import { pilotFxRingCss } from "../ui/components/ring.js";
5
- import { WAYGRAPH_RING_CSS } from "../highlights.js";
6
4
  // Real, direct user request: this overlay's own colors were an improvised
7
5
  // purple/white scheme (#a78bfa / #1a1033) that didn't match anything else -
8
6
  // "our design here seems like barely shares with the ui on the stepper
@@ -64,26 +62,9 @@ const OVERLAY_CSS = `
64
62
  opacity: 0; transform: translateY(4px); transition: opacity 0.15s, transform 0.15s;
65
63
  }
66
64
  #wg-pilot-activity.wg-pilot-toast-show { opacity: 1; transform: translateY(0); }
67
- /* Agent-sent fixture rings: generated from ui/tokens (tones shared with #wg-ring). */
68
- ${pilotFxRingCss()}
69
- #wg-pilot-fx-todos{
70
- position:fixed;z-index:2147483645;top:72px;max-width:280px;max-height:calc(100vh - 100px);overflow:auto;
71
- font:12px/1.4 ui-sans-serif,system-ui,sans-serif;color:#fff;
72
- background:${SURFACE};border-radius:10px;padding:10px 12px;
73
- box-shadow:0 2px 10px rgba(0,0,0,.3);pointer-events:auto;
74
- transition:opacity .25s ease;
75
- }
76
- #wg-pilot-fx-todos.wg-todo-behind{z-index:2147483643;opacity:.42;pointer-events:none;}
77
- #wg-pilot-fx-todos[data-pos=left]{left:14px;right:auto;}
78
- #wg-pilot-fx-todos[data-pos=right]{right:14px;left:auto;}
79
- #wg-pilot-fx-todos .wg-pilot-fx-todo-title{font-weight:700;color:#c9a6ff;margin-bottom:6px;}
80
- #wg-pilot-fx-todos ol{margin:0;padding-left:18px;}
81
- #wg-pilot-fx-todos li{margin:3px 0;color:#ddd;}
82
- #wg-pilot-fx-todos li.wg-pilot-fx-todo-done{color:#888;text-decoration:line-through;}
83
- #wg-pilot-fx-todos li.wg-pilot-fx-todo-now{color:#fff;font-weight:700;}
84
- #wg-pilot-fx-todos[data-compact="1"]:not(:hover) li.wg-todo-fold{display:none;}
85
- #wg-pilot-fx-todos .wg-todo-more{margin-top:6px;padding:4px 8px;border-radius:8px;
86
- background:rgba(124,58,237,.22);font:700 11px/1.2 system-ui,sans-serif;color:#e8dcff;}
65
+ /* Agent fixture rings (.wg-pilot-fx-ring/label), #wg-pilot-fx-todos, #wg-ring/#wg-ring-label and
66
+ #wg-banner all come from the ONE shared bundle ensureShadowRoot installs (ui/css/*.css) - not
67
+ redefined here. */
87
68
  /* Spotlight (demo focus:true) - dim page, cutout around target. */
88
69
  #wg-pilot-fx-focus{position:fixed;z-index:2147483644;pointer-events:none;
89
70
  border-radius:12px;box-shadow:0 0 0 9999px rgba(8,4,20,.62);
@@ -107,7 +88,7 @@ ${pilotFxRingCss()}
107
88
  #wg-pilot-fx-device[data-preset=mobile]{border-color:#3B82F6;}
108
89
  #wg-pilot-fx-device[data-preset=tablet]{border-color:#22C55E;}
109
90
  #wg-pilot-fx-device[data-preset=desktop]{border-color:#9CA3AF;}
110
- ` + WAYGRAPH_RING_CSS;
91
+ `;
111
92
  /** Browser-side shell install — runs on every document via context.addInitScript. */
112
93
  function installPilotOverlayShell(css) {
113
94
  if (__wgById("wg-pilot-overlay"))
@@ -2,3 +2,4 @@ export * from "./pilot-overlay/shell.js";
2
2
  export * from "./pilot-overlay/fixtures.js";
3
3
  export * from "./pilot-overlay/panel.js";
4
4
  export * from "./pilot-overlay/action-ring.js";
5
+ export * from "./pilot-overlay/overlay-fixtures.js";
@@ -3,3 +3,4 @@ export * from "./pilot-overlay/shell.js";
3
3
  export * from "./pilot-overlay/fixtures.js";
4
4
  export * from "./pilot-overlay/panel.js";
5
5
  export * from "./pilot-overlay/action-ring.js";
6
+ export * from "./pilot-overlay/overlay-fixtures.js";
@@ -0,0 +1,62 @@
1
+ // Banner (title card) authoring + verbose log helpers. Mirrors the todo dock's probe/log path
2
+ // so an agent without eyes can read banner state from the demo log.
3
+ import { demoLog } from "./demo-log.js";
4
+
5
+ /** Authored banner UX from a stub phase -> the { pos, collision, hidden } shape installOverlay takes. */
6
+ export function bannerUiFromPhase(phase) {
7
+ const ui = {};
8
+ if (phase && phase.titlePos) ui.pos = phase.titlePos;
9
+ if (phase && phase.bannerUi) {
10
+ if (phase.bannerUi.collision !== undefined) ui.collision = !!phase.bannerUi.collision;
11
+ if (phase.bannerUi.hidden !== undefined) ui.hidden = !!phase.bannerUi.hidden;
12
+ }
13
+ return ui;
14
+ }
15
+
16
+ /** Read the live banner: text, position, whether it was moved by ring collision; drains the move log. */
17
+ export async function probeBanner(page) {
18
+ return page
19
+ .evaluate(() => {
20
+ const b = __wgById("wg-banner");
21
+ const log = window.__wgBannerLog || [];
22
+ window.__wgBannerLog = [];
23
+ if (!b) return { present: false, log };
24
+ const r = b.getBoundingClientRect();
25
+ const text = b.querySelector(".wg-banner-text");
26
+ return {
27
+ present: true,
28
+ text: text ? text.textContent : "",
29
+ pos: b.dataset.pos || null,
30
+ home: b.dataset.home || null,
31
+ moved: b.dataset.moved || null,
32
+ collision: b.dataset.collision !== "0",
33
+ hidden: b.style.display === "none",
34
+ authored: b.dataset.authored === "1",
35
+ left: Math.round(r.left),
36
+ top: Math.round(r.top),
37
+ w: Math.round(r.width),
38
+ h: Math.round(r.height),
39
+ log,
40
+ };
41
+ })
42
+ .catch(() => ({ present: false, log: [] }));
43
+ }
44
+
45
+ export function logBannerDom(tag, p) {
46
+ if (!p) return;
47
+ if (!p.present) {
48
+ demoLog(" " + tag + " banner-dom absent");
49
+ return;
50
+ }
51
+ demoLog(
52
+ " " + tag + " banner-dom " +
53
+ JSON.stringify(String(p.text || "").slice(0, 60)) +
54
+ "@" + p.pos +
55
+ (p.home && p.home !== p.pos ? " (home=" + p.home + ")" : "") +
56
+ " " + p.left + "," + p.top + " " + p.w + "x" + p.h +
57
+ " collision=" + (p.collision ? "on" : "off") +
58
+ (p.hidden ? " HIDDEN" : "") +
59
+ (p.authored ? " authored" : ""),
60
+ );
61
+ for (const line of p.log || []) demoLog(" " + tag + " banner " + line);
62
+ }
@@ -109,8 +109,14 @@ 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
+
112
118
  function demoPrefix() {
113
- return ansiPaint(ANSI.dim, "[" + demoTs() + "]") + " waygraph demo: ";
119
+ return ansiPaint(ANSI.dim, "[" + demoTs() + "]") + " waygraph " + logSurface + ": ";
114
120
  }
115
121
 
116
122
  export function demoLog(msg) {
@@ -170,6 +176,8 @@ export function logStubPhaseFixtures(phaseName, phase) {
170
176
  const tag = "stub." + phaseName;
171
177
  if (phase.title) demoFixture(tag, "ctx.title(...)", JSON.stringify(phase.title));
172
178
  if (phase.banner) demoFixture(tag, "ctx.banner(...)", JSON.stringify(phase.banner));
179
+ if (phase.titlePos) demoFixture(tag, "ctx.titlePos(...)", JSON.stringify(phase.titlePos));
180
+ if (phase.bannerUi) demoFixture(tag, "ctx.bannerUi(...)", JSON.stringify(phase.bannerUi));
173
181
  if (phase.deviceSync && phase.deviceSync !== "keep") {
174
182
  demoFixture(
175
183
  tag,