waygraph 0.15.31 → 0.15.33

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 (69) hide show
  1. package/dist/auto-session/session.js +2 -1
  2. package/dist/highlights/run-phase.js +2 -0
  3. package/dist/highlights/stub-ctx.js +36 -2
  4. package/dist/highlights/types.d.ts +35 -3
  5. package/dist/index.d.ts +1 -1
  6. package/dist/pilot-overlay/action-ring.d.ts +16 -0
  7. package/dist/pilot-overlay/action-ring.js +67 -0
  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 +2 -0
  14. package/dist/pilot-overlay.js +2 -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/components/ring.js +5 -0
  28. package/dist/ui/overlay.css +209 -0
  29. package/dist/ui/shadow.d.ts +7 -2
  30. package/dist/ui/shadow.js +12 -4
  31. package/dist/ui/stylesheet.d.ts +7 -0
  32. package/dist/ui/stylesheet.js +18 -0
  33. package/dist/verbose-log.d.ts +1 -0
  34. package/dist/verbose-log.js +16 -0
  35. package/examples/saucedemo/.waygraph-auto/03df352d.log +0 -0
  36. package/examples/saucedemo/.waygraph-auto/0a8c3719.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/2c79c73a.log +0 -0
  40. package/examples/saucedemo/.waygraph-auto/2c94a05b.log +0 -0
  41. package/examples/saucedemo/.waygraph-auto/2f4f531e.log +0 -0
  42. package/examples/saucedemo/.waygraph-auto/37b56184.log +0 -0
  43. package/examples/saucedemo/.waygraph-auto/3bdb2f5a.log +0 -0
  44. package/examples/saucedemo/.waygraph-auto/476cff76.log +0 -0
  45. package/examples/saucedemo/.waygraph-auto/535e8922.log +0 -0
  46. package/examples/saucedemo/.waygraph-auto/5b838485.log +0 -0
  47. package/examples/saucedemo/.waygraph-auto/5d13bdde.log +0 -0
  48. package/examples/saucedemo/.waygraph-auto/63b0f4cd.log +0 -0
  49. package/examples/saucedemo/.waygraph-auto/6f05bdc1.log +0 -0
  50. package/examples/saucedemo/.waygraph-auto/70279fc3.log +0 -0
  51. package/examples/saucedemo/.waygraph-auto/731a14bd.log +0 -0
  52. package/examples/saucedemo/.waygraph-auto/73a41843.log +0 -0
  53. package/examples/saucedemo/.waygraph-auto/76128a2d.log +0 -0
  54. package/examples/saucedemo/.waygraph-auto/796ab578.log +0 -0
  55. package/examples/saucedemo/.waygraph-auto/8598b4c0.log +0 -0
  56. package/examples/saucedemo/.waygraph-auto/859a261d.log +0 -0
  57. package/examples/saucedemo/.waygraph-auto/a1ab5d29.log +0 -0
  58. package/examples/saucedemo/.waygraph-auto/a94738b7.log +0 -0
  59. package/examples/saucedemo/.waygraph-auto/b3d172f3.log +0 -0
  60. package/examples/saucedemo/.waygraph-auto/b57921c5.log +0 -0
  61. package/examples/saucedemo/.waygraph-auto/b9ab0983.log +0 -0
  62. package/examples/saucedemo/.waygraph-auto/bf8693e7.log +0 -0
  63. package/examples/saucedemo/.waygraph-auto/d37719a6.log +0 -0
  64. package/examples/saucedemo/.waygraph-auto/da0a06bf.log +0 -0
  65. package/examples/saucedemo/.waygraph-auto/dc58eb72.log +0 -0
  66. package/examples/saucedemo/.waygraph-auto/f86224c6.log +0 -0
  67. package/examples/saucedemo/.waygraph-auto/fe83305d.log +0 -0
  68. package/examples/saucedemo/.waygraph-auto/ff616932.log +0 -0
  69. package/package.json +3 -2
@@ -1,6 +1,6 @@
1
1
  // Split out of the former 1,078-line auto-session.ts (see src/ARCHITECTURE.md). Behavior unchanged.
2
2
  import { CONSOLE_LOG_MAX_ENTRIES, FULL_MODE_DEFAULT_DEPTH, FULL_MODE_MAX_NODES, FULL_MODE_MAX_TEXT_LENGTH, TRACE_MAX_STEPS, buildSessionSnapshot, parsePick, stubFilePath, stubPhaseHasContent, walkFullDom } from "./helpers.js";
3
- import { installPersistentPilotOverlay, showPilotActivity, showPilotFixtures, showPilotVision, updatePilotOverlay } from "../pilot-overlay.js";
3
+ import { instrumentPilotActions, installPersistentPilotOverlay, showPilotActivity, showPilotFixtures, showPilotVision, updatePilotOverlay } from "../pilot-overlay.js";
4
4
  import { buildExploreContext, buildExploreMenu } from "../auto-explore.js";
5
5
  import { MemPage } from "../mem-page.js";
6
6
  import { Engine } from "../engine.js";
@@ -92,6 +92,7 @@ export class AutoSession {
92
92
  const context = await browser.newContext(contextOpts);
93
93
  await installPersistentPilotOverlay(context);
94
94
  const page = await context.newPage();
95
+ instrumentPilotActions(page, headless ? 0 : 450);
95
96
  if (startUrl) {
96
97
  await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
97
98
  await page.waitForLoadState("load").catch(() => { });
@@ -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";
@@ -0,0 +1,16 @@
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
+ /**
11
+ * Every Playwright instruction a Block runs against this session's page gets a gray (auto) ring + robot
12
+ * label first, so a human watching a pilot window sees WHAT is being driven. `dwellMs` holds the ring
13
+ * long enough to read before the action fires (0 for headless).
14
+ */
15
+ export declare function instrumentPilotActions(page: Page, dwellMs: number): void;
16
+ export {};
@@ -0,0 +1,67 @@
1
+ import { ensureInstalled } from "./shell.js";
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") {
4
+ await ensureInstalled(page);
5
+ await page
6
+ .evaluate(({ box, label, tone }) => {
7
+ const ring = __wgById("wg-ring");
8
+ const tag = __wgById("wg-ring-label");
9
+ if (!ring || !tag)
10
+ return;
11
+ const w = window;
12
+ if (w.__wgVisionHideTimer)
13
+ clearTimeout(w.__wgVisionHideTimer);
14
+ ring.dataset.tone = tone;
15
+ ring.dataset.size = "md";
16
+ tag.dataset.tone = tone;
17
+ tag.dataset.size = "md";
18
+ tag.dataset.weight = "normal";
19
+ ring.style.left = `${box.x - 3}px`;
20
+ ring.style.top = `${box.y - 3}px`;
21
+ ring.style.width = `${box.width + 6}px`;
22
+ ring.style.height = `${box.height + 6}px`;
23
+ tag.textContent = label;
24
+ tag.style.left = `${Math.max(0, box.x)}px`;
25
+ tag.style.top = `${Math.max(0, box.y - 24)}px`;
26
+ ring.style.opacity = "1";
27
+ tag.style.opacity = "1";
28
+ w.__wgVisionHideTimer = setTimeout(() => {
29
+ ring.style.opacity = "0";
30
+ tag.style.opacity = "0";
31
+ }, 4_000);
32
+ }, { box, label, tone })
33
+ .catch(() => { });
34
+ }
35
+ const LOCATOR_ACTIONS = ["click", "dblclick", "hover", "tap", "fill", "check", "uncheck", "selectOption", "press", "pressSequentially", "setInputFiles"];
36
+ const patched = new WeakSet();
37
+ /**
38
+ * Every Playwright instruction a Block runs against this session's page gets a gray (auto) ring + robot
39
+ * label first, so a human watching a pilot window sees WHAT is being driven. `dwellMs` holds the ring
40
+ * long enough to read before the action fires (0 for headless).
41
+ */
42
+ export function instrumentPilotActions(page, dwellMs) {
43
+ const proto = Object.getPrototypeOf(page.locator("html"));
44
+ if (patched.has(proto))
45
+ return;
46
+ patched.add(proto);
47
+ for (const action of LOCATOR_ACTIONS) {
48
+ const original = proto[action];
49
+ if (typeof original !== "function")
50
+ continue;
51
+ proto[action] = async function (...args) {
52
+ try {
53
+ const box = await this.first().boundingBox({ timeout: 1_500 });
54
+ if (box) {
55
+ const desc = String(this).replace(/^locator\(['"]?|['"]?\)$/g, "").slice(0, 60);
56
+ await showPilotRect(this.page(), box, `${action} ${desc}`);
57
+ if (dwellMs > 0)
58
+ await this.page().waitForTimeout(dwellMs);
59
+ }
60
+ }
61
+ catch {
62
+ /* decoration only - never block the real action */
63
+ }
64
+ return original.apply(this, args);
65
+ };
66
+ }
67
+ }
@@ -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"))
@@ -1,3 +1,5 @@
1
1
  export * from "./pilot-overlay/shell.js";
2
2
  export * from "./pilot-overlay/fixtures.js";
3
3
  export * from "./pilot-overlay/panel.js";
4
+ export * from "./pilot-overlay/action-ring.js";
5
+ export * from "./pilot-overlay/overlay-fixtures.js";
@@ -2,3 +2,5 @@
2
2
  export * from "./pilot-overlay/shell.js";
3
3
  export * from "./pilot-overlay/fixtures.js";
4
4
  export * from "./pilot-overlay/panel.js";
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,
@@ -1,10 +1,11 @@
1
1
  // In-page installer (runs inside the browser via page.evaluate): must stay self-contained - no module-scope closure.
2
2
  // Split out of the former single installOverlay evaluate (see src/ARCHITECTURE.md). Shares state only via window.__wg*.
3
3
  /* eslint-disable no-unused-vars */
4
- export function installBanner({ title, favicon, bannerPos, todoPos, envAutoplay, todoDockUi }) {
4
+ export function installBanner({ title, favicon, bannerPos, todoPos, envAutoplay, todoDockUi, bannerUi }) {
5
5
  const POSITIONS = ["left", "center", "right"];
6
6
  const applyPos = (el, pos) => {
7
7
  el.dataset.pos = pos;
8
+ el.dataset.home = pos;
8
9
  try {
9
10
  localStorage.setItem("wg-banner-pos", pos);
10
11
  } catch {
@@ -52,6 +53,60 @@ export function installBanner({ title, favicon, bannerPos, todoPos, envAutoplay,
52
53
  text.textContent = title;
53
54
  }
54
55
  }
56
+ // Authored banner UX (StubCtx.titlePos / bannerUi): applied every step, wins over click/env.
57
+ if (bannerUi) {
58
+ const cur = window.__wgBannerUi || {};
59
+ window.__wgBannerUi = { ...cur, ...bannerUi };
60
+ }
61
+ const bui = window.__wgBannerUi || {};
62
+ const bannerEl = __wgById("wg-banner");
63
+ if (bannerEl) {
64
+ if (bui.pos && POSITIONS.includes(bui.pos) && bannerEl.dataset.home !== bui.pos) {
65
+ bannerEl.dataset.pos = bui.pos;
66
+ bannerEl.dataset.home = bui.pos;
67
+ bannerEl.dataset.authored = "1";
68
+ }
69
+ bannerEl.dataset.collision = bui.collision === false ? "0" : "1";
70
+ bannerEl.style.display = bui.hidden ? "none" : "";
71
+ }
72
+ // Move the banner out of the way of a highlight ring (mirrors the todo dock's collision flip).
73
+ window.__wgBannerAvoidRing = (box) => {
74
+ const b = __wgById("wg-banner");
75
+ if (!b || !box || b.dataset.collision === "0" || b.style.display === "none") return;
76
+ const pad = 10;
77
+ const hits = () => {
78
+ const r = b.getBoundingClientRect();
79
+ return !(
80
+ r.right < box.x - pad ||
81
+ r.left > box.x + box.width + pad ||
82
+ r.bottom < box.y - pad ||
83
+ r.top > box.y + box.height + pad
84
+ );
85
+ };
86
+ const home = b.dataset.home || b.dataset.pos || "left";
87
+ b.dataset.pos = home;
88
+ if (!hits()) {
89
+ delete b.dataset.moved;
90
+ return;
91
+ }
92
+ for (const cand of POSITIONS.filter((p) => p !== home)) {
93
+ b.dataset.pos = cand;
94
+ if (!hits()) {
95
+ b.dataset.moved = home + ">" + cand;
96
+ (window.__wgBannerLog = window.__wgBannerLog || []).push("moved " + home + " -> " + cand + " (ring overlap)");
97
+ return;
98
+ }
99
+ }
100
+ b.dataset.pos = home;
101
+ b.dataset.moved = "none-free";
102
+ (window.__wgBannerLog = window.__wgBannerLog || []).push("overlap at every position (kept " + home + ")");
103
+ };
104
+ window.__wgBannerRestore = () => {
105
+ const b = __wgById("wg-banner");
106
+ if (!b) return;
107
+ if (b.dataset.home) b.dataset.pos = b.dataset.home;
108
+ delete b.dataset.moved;
109
+ };
55
110
  // Tab title/favicon: a real navigation resets document.title and any
56
111
  // <link rel="icon"> the new document brings, so re-check (not
57
112
  // re-append) on every call instead of a one-time flag.
@@ -114,6 +114,7 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
114
114
  ringLabel.style.top = labelTop + "px";
115
115
  if (window.__wgTodosSetBehind) window.__wgTodosSetBehind(true);
116
116
  if (window.__wgTodosAvoidRingCollision) window.__wgTodosAvoidRingCollision(box);
117
+ if (window.__wgBannerAvoidRing) window.__wgBannerAvoidRing(box);
117
118
  };
118
119
  /** Dim/lower floating todo docks while a ring caption is the focus. */
119
120
  window.__wgTodosSetBehind = (on) => {
@@ -220,6 +221,7 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
220
221
  if (ringLabel) ringLabel.style.opacity = "0";
221
222
  if (window.__wgClearFocus) window.__wgClearFocus();
222
223
  if (window.__wgTodosSetBehind) window.__wgTodosSetBehind(false);
224
+ if (window.__wgBannerRestore) window.__wgBannerRestore();
223
225
  window.__wgTodoCollisionLocked = false;
224
226
  __wgQA(".wg-todo-dock[data-tucked], #wg-todo-dock[data-tucked]").forEach((el) => {
225
227
  delete el.dataset.tucked;