waygraph 0.16.0 → 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 (143) hide show
  1. package/dist/cli/commands/browser.d.ts +2 -0
  2. package/dist/cli/commands/browser.js +109 -0
  3. package/dist/cli/commands/codemod-stubs.js +90 -2
  4. package/dist/cli/commands/pilot.d.ts +2 -0
  5. package/dist/cli/commands/pilot.js +102 -0
  6. package/dist/cli/commands/session-ops.d.ts +3 -0
  7. package/dist/cli/commands/session-ops.js +257 -0
  8. package/dist/cli/commands/session.js +20 -461
  9. package/dist/cli/run-chain.js +2 -2
  10. package/dist/cli/usage.d.ts +1 -1
  11. package/dist/cli/util.js +1 -1
  12. package/dist/demo/narration/demo-pace.d.ts +2 -2
  13. package/dist/demo/narration/demo-pace.js +1 -1
  14. package/dist/demo/narration/device.d.ts +2 -2
  15. package/dist/demo/narration/merge.d.ts +5 -3
  16. package/dist/demo/narration/merge.js +3 -7
  17. package/dist/demo/narration/narration.d.ts +9 -13
  18. package/dist/demo/narration/narration.js +5 -17
  19. package/dist/demo/narration/stub-ctx.js +3 -3
  20. package/dist/demo/narration/todo-dock.d.ts +5 -5
  21. package/dist/demo/overlay/cursor.d.ts +9 -2
  22. package/dist/demo/overlay/cursor.js +1 -1
  23. package/dist/demo/overlay/device-stage.d.ts +15 -2
  24. package/dist/demo/overlay/inpage/banner.d.ts +2 -1
  25. package/dist/demo/overlay/inpage/banner.js +3 -3
  26. package/dist/demo/overlay/inpage/caption.d.ts +1 -1
  27. package/dist/demo/overlay/inpage/core.d.ts +2 -1
  28. package/dist/demo/overlay/inpage/core.js +1 -7
  29. package/dist/demo/overlay/inpage/device-chip.d.ts +1 -1
  30. package/dist/demo/overlay/inpage/device-chip.js +6 -6
  31. package/dist/demo/overlay/inpage/network.d.ts +3 -2
  32. package/dist/demo/overlay/inpage/network.js +1 -12
  33. package/dist/demo/overlay/inpage/panel-chrome.d.ts +1 -1
  34. package/dist/demo/overlay/inpage/panel-chrome.js +1 -1
  35. package/dist/demo/overlay/inpage/stamp.d.ts +1 -1
  36. package/dist/demo/overlay/inpage/stamp.js +1 -1
  37. package/dist/demo/overlay/inpage/swipe-zoom.d.ts +1 -1
  38. package/dist/demo/overlay/inpage/swipe-zoom.js +1 -1
  39. package/dist/demo/overlay/inpage/todos.d.ts +2 -1
  40. package/dist/demo/overlay/inpage/todos.js +1 -2
  41. package/dist/demo/overlay/network-capture.d.ts +2 -2
  42. package/dist/demo/overlay/network-capture.js +1 -1
  43. package/dist/demo/overlay/overlay-css.js +1 -55
  44. package/dist/demo/overlay/overlay-stage.d.ts +19 -11
  45. package/dist/demo/overlay/overlay.d.ts +2 -2
  46. package/dist/demo/overlay/overlay.js +3 -2
  47. package/dist/demo/overlay/rings.d.ts +38 -18
  48. package/dist/demo/overlay/rings.js +21 -62
  49. package/dist/demo/overlay/ui/overlay.css +40 -0
  50. package/dist/demo/stepper/after.js +11 -34
  51. package/dist/demo/stepper/banner-log.d.ts +4 -6
  52. package/dist/demo/stepper/banner-log.js +3 -1
  53. package/dist/demo/stepper/before.js +6 -4
  54. package/dist/demo/stepper/demo-log.d.ts +26 -9
  55. package/dist/demo/stepper/demo-log.js +10 -27
  56. package/dist/demo/stepper/interaction-patch.d.ts +3 -2
  57. package/dist/demo/stepper/interaction-patch.js +22 -34
  58. package/dist/demo/stepper/panel-after.d.ts +17 -0
  59. package/dist/demo/stepper/panel-after.js +166 -0
  60. package/dist/demo/stepper/panel-before.d.ts +12 -0
  61. package/dist/demo/stepper/panel-before.js +241 -0
  62. package/dist/demo/stepper/slides.d.ts +16 -1
  63. package/dist/demo/stepper/slides.js +21 -5
  64. package/dist/demo/stepper/step-panels.d.ts +57 -9
  65. package/dist/demo/stepper/step-panels.js +10 -427
  66. package/dist/demo/stepper/stepper.js +1 -1
  67. package/dist/demo/stepper/teardown.d.ts +4 -5
  68. package/dist/demo/stepper/teardown.js +4 -6
  69. package/dist/demo/stepper/todo-advance.d.ts +3 -2
  70. package/dist/demo/stepper/todo-advance.js +2 -2
  71. package/dist/demo/stepper/view.d.ts +4 -14
  72. package/dist/demo/types.d.ts +69 -1
  73. package/dist/demo/verbose-log.js +1 -1
  74. package/dist/engine/block/action-page.d.ts +53 -0
  75. package/dist/engine/block/action-page.js +1 -0
  76. package/dist/engine/block/block.d.ts +24 -90
  77. package/dist/engine/block/block.js +2 -77
  78. package/dist/engine/block/branch.d.ts +17 -0
  79. package/dist/engine/block/branch.js +40 -0
  80. package/dist/engine/block/dialog.d.ts +13 -0
  81. package/dist/engine/block/dialog.js +37 -0
  82. package/dist/engine/block/kinds/method.d.ts +0 -2
  83. package/dist/engine/block/kinds/method.js +0 -1
  84. package/dist/engine/block/kinds/nav.js +1 -1
  85. package/dist/engine/flow/map-branch.d.ts +9 -0
  86. package/dist/engine/flow/map-branch.js +57 -0
  87. package/dist/engine/flow/map-builder.d.ts +7 -22
  88. package/dist/engine/flow/map-builder.js +6 -145
  89. package/dist/engine/flow/map-steps.d.ts +30 -0
  90. package/dist/engine/flow/map-steps.js +87 -0
  91. package/dist/engine/run/run.d.ts +1 -19
  92. package/dist/engine/run/run.js +0 -88
  93. package/dist/engine/stub/ring.d.ts +2 -4
  94. package/dist/engine/stub/ring.js +1 -0
  95. package/dist/engine/stub/stub.d.ts +16 -8
  96. package/dist/engine/trait/trait.js +1 -1
  97. package/dist/index.d.ts +15 -12
  98. package/dist/index.js +9 -7
  99. package/dist/live/auto/auto.d.ts +3 -15
  100. package/dist/live/auto/auto.js +10 -420
  101. package/dist/live/auto/explore.d.ts +2 -2
  102. package/dist/live/auto/mem.d.ts +11 -0
  103. package/dist/live/auto/mem.js +85 -0
  104. package/dist/live/auto/menu.d.ts +30 -0
  105. package/dist/live/auto/menu.js +335 -0
  106. package/dist/live/auto-session/auto-session.d.ts +23 -103
  107. package/dist/live/auto-session/auto-session.js +100 -370
  108. package/dist/live/auto-session/console.d.ts +4 -0
  109. package/dist/live/auto-session/console.js +41 -0
  110. package/dist/live/auto-session/files.d.ts +44 -0
  111. package/dist/live/auto-session/files.js +145 -0
  112. package/dist/live/auto-session/helpers.d.ts +5 -2
  113. package/dist/live/auto-session/helpers.js +17 -0
  114. package/dist/live/auto-session/inspect.d.ts +4 -0
  115. package/dist/live/auto-session/inspect.js +75 -0
  116. package/dist/live/auto-session/ipc.d.ts +3 -22
  117. package/dist/live/auto-session/ipc.js +7 -147
  118. package/dist/live/auto-session/open-browser.d.ts +6 -0
  119. package/dist/live/auto-session/open-browser.js +47 -0
  120. package/dist/live/pilot/pilot.d.ts +1 -1
  121. package/dist/live/pilot/pilot.js +2 -1
  122. package/dist/live/traverse/traverse.js +6 -200
  123. package/dist/live/traverse/worker.d.ts +37 -0
  124. package/dist/live/traverse/worker.js +200 -0
  125. package/dist/runner/plan/find.js +2 -2
  126. package/dist/runner/plan/plan.js +1 -1
  127. package/dist/runner/plan/seed-mem.js +5 -5
  128. package/dist/runner/plan/spec.js +1 -1
  129. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
  130. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +2 -1
  131. package/examples/saucedemo/src/flows/checkout.flow.ts +2 -1
  132. package/examples/saucedemo/src/flows/shop.flow.ts +2 -1
  133. package/examples/saucedemo/src/stubs/device-showcase.ts +37 -43
  134. package/examples/saucedemo/src/stubs/finish-order.ts +38 -0
  135. package/package.json +5 -1
  136. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
  137. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +2 -1
  138. package/templates/quickstart/src/flows/checkout.flow.ts +2 -1
  139. package/templates/quickstart/src/flows/shop.flow.ts +2 -1
  140. package/templates/quickstart/src/stubs/device-showcase.ts +37 -43
  141. package/templates/quickstart/src/stubs/finish-order.ts +38 -0
  142. package/templates/scaffold/src/flows/home/hello.flow.ts +3 -0
  143. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-hello.method.block.ts +1 -9
@@ -1,28 +1,34 @@
1
- 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";
2
- import { keepOverlayOnNavigation } from "../../demo/overlay/reattach.js";
3
- import { installPersistentPilotOverlay, showPilotActivity, showPilotVision } from "../../demo/overlay/pilot/shell.js";
4
- import { showPilotFixtures } from "../../demo/overlay/pilot/fixtures.js";
5
- import { instrumentPilotActions } from "../../demo/overlay/pilot/actions.js";
6
- import { phaseToOverlayFixtures, describeOverlayFixtures } from "../../demo/overlay/phase-fixtures.js";
7
- import { updatePilotOverlay } from "../pilot/panel.js";
8
- import { buildExploreContext, buildExploreMenu } from "../auto/explore.js";
9
- import { MemPage } from "../../engine/mem/mem.js";
10
- import { Engine } from "../../engine/engine.js";
11
- import { detectHere, ensureLivePage, ensureMem, runOneBlock, seedDefaultMem } from "../auto/auto.js";
12
1
  import { collectKnownInteractionSelectors, unmappedInteractionsPageScript } from "../../analysis/coverage-gap.js";
2
+ import { findBlockPathDetailed } from "../../analysis/graph.js";
13
3
  import { runStubPhase } from "../../demo/narration/narration.js";
4
+ import { describeOverlayFixtures, phaseToOverlayFixtures } from "../../demo/overlay/phase-fixtures.js";
5
+ import { showPilotFixtures } from "../../demo/overlay/pilot/fixtures.js";
6
+ import { showPilotActivity, showPilotVision } from "../../demo/overlay/pilot/shell.js";
7
+ import { keepOverlayOnNavigation } from "../../demo/overlay/reattach.js";
14
8
  import { verboseLog } from "../../demo/verbose-log.js";
15
- import { findBlockPathDetailed } from "../../analysis/graph.js";
16
- import { configuredExtensions, resolveBaseUrl } from "../../runtime/waygraph-config.js";
17
- import { chromiumExecutablePath, launchContext } from "../../runtime/browser.js";
18
- import { ignorePageGone, ignoreTimeout } from "../../engine/page-gone.js";
9
+ import { Engine } from "../../engine/engine.js";
10
+ import { MemPage } from "../../engine/mem/mem.js";
11
+ import { ignorePageGone } from "../../engine/page-gone.js";
12
+ import { resolveBaseUrl } from "../../runtime/waygraph-config.js";
13
+ import { detectHere, ensureLivePage, runOneBlock } from "../auto/auto.js";
14
+ import { buildExploreContext, buildExploreMenu } from "../auto/explore.js";
15
+ import { ensureMem, seedDefaultMem } from "../auto/mem.js";
16
+ import { updatePilotOverlay } from "../pilot/panel.js";
17
+ import { recordPageConsole } from "./console.js";
18
+ import { CONSOLE_LOG_MAX_ENTRIES, TRACE_MAX_STEPS, buildSessionSnapshot, describeHighlight, parsePick, stubFilePath, stubPhaseHasContent } from "./helpers.js";
19
+ import { inspectDom, inspectStorage } from "./inspect.js";
20
+ import { openSessionBrowser } from "./open-browser.js";
21
+ /** The first element `selector` matches, ringed with the pilot's vision ring; or why there is none. */
22
+ async function rawTarget(page, selector, label) {
23
+ const locator = page.locator(selector).first();
24
+ if ((await locator.count()) === 0)
25
+ return `no element matches selector "${selector}"`;
26
+ await showPilotVision(page, selector, label);
27
+ return locator;
28
+ }
19
29
  /**
20
- * Non-interactive explore session: same graph/library/mem/browser setup
21
- * `runAutoExplore` runs in `--cli` mode, driven by `applyPick` instead of a
22
- * blocking `readline` loop. Headless by default (matching
23
- * `runAutoExplore`'s own `cli` branch), optionally visible via
24
- * `AutoSessionInit.headless: false` - this type only ever backs the `--cli`
25
- * picker, never the headful page-embedded panel, headless or not.
30
+ * A live session driven over IPC (`browser start`, `pilot start`): the same graph, library, mem and browser as
31
+ * `waygraph auto`, moved one call at a time (`applyPick`, `applyPath`, the raw actions) instead of a menu loop.
26
32
  */
27
33
  export class AutoSession {
28
34
  projectDir;
@@ -73,45 +79,7 @@ export class AutoSession {
73
79
  seedDefaultMem(library.byName, mem);
74
80
  const headless = init.headless ?? true;
75
81
  const engine = new Engine({ headless });
76
- const { chromium } = await import("@playwright/test");
77
- const executablePath = chromiumExecutablePath();
78
- // Real gap found live: a headed (--non-headless) session kept a fixed
79
- // 1280x720 content area even when the user maximized/fullscreened the
80
- // actual window - `viewport: null` alone only makes the PAGE track the
81
- // window; the window itself still launches at Chromium's own small
82
- // default size unless told to start maximized. Same fix cli.ts's own
83
- // --step/headed launch already uses (see its own comment there) -
84
- // headless keeps the fixed viewport since there's no real window to size.
85
- const launchOpts = {
86
- headless,
87
- // Keep Playwright's default --no-startup-window for headful: without it
88
- // Chromium opens its own "New Tab" window AND our context.newPage() opens
89
- // a second about:blank window. bringToFront() below surfaces the real page.
90
- args: headless ? [] : ["--start-maximized"],
91
- };
92
- if (executablePath)
93
- launchOpts.executablePath = executablePath;
94
- const contextOpts = {
95
- viewport: headless ? { width: 1280, height: 720 } : null,
96
- };
97
- if (baseURL)
98
- contextOpts.baseURL = baseURL;
99
- const { close: closeBrowser, context } = await launchContext({
100
- launcher: chromium,
101
- launch: launchOpts,
102
- context: contextOpts,
103
- extensions: await configuredExtensions(init.projectDir),
104
- });
105
- await installPersistentPilotOverlay(context);
106
- const page = await context.newPage();
107
- instrumentPilotActions(page, headless ? 0 : 450);
108
- if (startUrl) {
109
- await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(ignorePageGone);
110
- await page.waitForLoadState("load").catch(ignoreTimeout);
111
- }
112
- else {
113
- await page.goto("about:blank").catch(ignorePageGone);
114
- }
82
+ const { closeBrowser, context, page } = await openSessionBrowser(init.projectDir, headless, baseURL, startUrl);
115
83
  const knownInteractionSelectors = collectKnownInteractionSelectors(init.projectDir, library, init.inject);
116
84
  const session = new AutoSession(init.projectDir, init.inject, init.blocksSelect, graph, library, mem, engine, closeBrowser, context, page, startUrl, init.sessionId, knownInteractionSelectors, headless);
117
85
  keepOverlayOnNavigation(page, () => session.repaintOverlayAfterNavigation());
@@ -119,44 +87,7 @@ export class AutoSession {
119
87
  await session.bootstrapOverlay().catch(ignorePageGone);
120
88
  await page.bringToFront().catch(ignorePageGone);
121
89
  }
122
- // Attached once, on the page created here - `ensureLivePage` only swaps
123
- // to a fresh Page if the current one closed (a rare case, e.g. the human
124
- // closing the tab), not on ordinary same-page SPA navigation, which is
125
- // all this target (and most real sites) actually do.
126
- page.on("console", (msg) => {
127
- session.pushConsoleLog({
128
- type: "console",
129
- level: msg.type(),
130
- text: msg.text(),
131
- timestamp: new Date().toISOString(),
132
- });
133
- });
134
- page.on("pageerror", (err) => {
135
- session.pushConsoleLog({
136
- type: "pageerror",
137
- text: err instanceof Error ? err.message : String(err),
138
- timestamp: new Date().toISOString(),
139
- });
140
- });
141
- page.on("response", (res) => {
142
- // Every failed response (the original case), PLUS every mutation
143
- // (non-GET) response regardless of status - a real gap found live:
144
- // "auto console" only logging failures made a SUCCESSFUL POST that
145
- // simply didn't navigate anywhere look identical to "nothing
146
- // happened at all," undiagnosable the same way a real failure was.
147
- // A mutation's outcome (2xx or not) is always diagnostically
148
- // relevant; a GET's usually isn't (assets, trackers) unless it failed.
149
- const method = res.request().method();
150
- if (res.status() < 400 && method === "GET")
151
- return;
152
- session.pushConsoleLog({
153
- type: "response",
154
- text: `${method} ${res.statusText()}`,
155
- url: res.url(),
156
- status: res.status(),
157
- timestamp: new Date().toISOString(),
158
- });
159
- });
90
+ recordPageConsole(page, (entry) => session.pushConsoleLog(entry));
160
91
  return session;
161
92
  }
162
93
  /** Pure getter - returns a copy, not the live array. */
@@ -183,14 +114,8 @@ export class AutoSession {
183
114
  this.here = await detectHere(this.page, this.library.navBlocks);
184
115
  }
185
116
  const menu = await buildExploreMenu(this.page, this.graph, this.library, this.here);
186
- // Every caller of currentMenu() (status reads, applyPick/applyPath,
187
- // the raw click/type/goto primitives, resync) already routes through
188
- // here, so hooking the overlay refresh at this one point covers every
189
- // real state-changing (and reading) path without needing a separate
190
- // call at each site. Awaited deliberately, not fire-and-forget - the
191
- // whole point is a human watching the screen sees the SAME state a
192
- // concurrent API caller just got back, not a stale frame that catches
193
- // up moments later.
117
+ // Every read and every action goes through currentMenu(), so the overlay is refreshed here, awaited: someone
118
+ // watching sees the state an API caller just got back, not a frame behind.
194
119
  this.lastOverlayInfo = {
195
120
  sessionId: this.sessionId,
196
121
  snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
@@ -250,94 +175,13 @@ export class AutoSession {
250
175
  */
251
176
  async inspectDom(opts) {
252
177
  this.page = await ensureLivePage(this.context, this.page, this.startUrl);
253
- const mode = opts.mode ?? "aria";
254
- const ariaOptions = opts.depth !== undefined ? { mode: "ai", depth: opts.depth } : { mode: "ai" };
255
- if (opts.selector) {
256
- const locator = this.page.locator(opts.selector).first();
257
- if ((await locator.count()) === 0) {
258
- return { ok: false, error: `no element matches selector "${opts.selector}"` };
259
- }
260
- await showPilotVision(this.page, opts.selector, `dom (${mode}): ${opts.selector}`, "orange");
261
- if (mode === "full") {
262
- const caps = {
263
- depth: opts.depth ?? FULL_MODE_DEFAULT_DEPTH,
264
- maxNodes: FULL_MODE_MAX_NODES,
265
- maxTextLength: FULL_MODE_MAX_TEXT_LENGTH,
266
- };
267
- const { tree, truncated } = await locator.evaluate(walkFullDom, caps);
268
- return { ok: true, snapshot: { mode: "full", selector: opts.selector, truncated, tree } };
269
- }
270
- const tree = await locator.ariaSnapshotJSON(ariaOptions);
271
- return { ok: true, snapshot: { mode: "aria", selector: opts.selector, truncated: false, tree } };
272
- }
273
- await showPilotActivity(this.page, `Running (Dom): ${mode}, whole page`);
274
- if (mode === "full") {
275
- // page.evaluate only ships the one function passed to it - it can't
276
- // reach another named function by closure. Routing the whole-page case
277
- // through the same `html` Locator lets both cases share one
278
- // self-contained walkFullDom instead of two near-duplicate callbacks.
279
- const caps = {
280
- depth: opts.depth ?? FULL_MODE_DEFAULT_DEPTH,
281
- maxNodes: FULL_MODE_MAX_NODES,
282
- maxTextLength: FULL_MODE_MAX_TEXT_LENGTH,
283
- };
284
- const { tree, truncated } = await this.page.locator("html").first().evaluate(walkFullDom, caps);
285
- return { ok: true, snapshot: { mode: "full", truncated, tree } };
286
- }
287
- const tree = await this.page.ariaSnapshotJSON(ariaOptions);
288
- return { ok: true, snapshot: { mode: "aria", truncated: false, tree } };
178
+ return inspectDom(this.page, opts);
289
179
  }
290
- /**
291
- * Pure getter - reads the live page's client-side state: localStorage,
292
- * sessionStorage, and registered service workers (scope/active URL/state).
293
- * Real, direct follow-up to `auto console`'s own gap: a site doing web
294
- * push (this target has a real `NOTIFICATION_VAPID` config, confirmed via
295
- * `auto dom` earlier) keeps push-subscription/auth state in these places,
296
- * not in the rendered DOM `inspectDom` reads.
297
- */
180
+ /** Reads the live page's client state: storage, cookies, service workers (where web-push state lives). */
298
181
  async inspectStorage() {
299
182
  this.page = await ensureLivePage(this.context, this.page, this.startUrl);
300
- const [localStorageEntries, sessionStorageEntries] = await this.page.evaluate(() => [
301
- Object.entries(window.localStorage),
302
- Object.entries(window.sessionStorage),
303
- ]);
304
- const cookies = await this.context.cookies();
305
- const workers = this.context.serviceWorkers();
306
- const registrations = await this.page
307
- .evaluate(async () => {
308
- if (!("serviceWorker" in navigator))
309
- return [];
310
- const regs = await navigator.serviceWorker.getRegistrations();
311
- return regs.map((r) => ({
312
- scope: r.scope,
313
- ...(r.active?.scriptURL ? { activeUrl: r.active.scriptURL } : {}),
314
- ...(r.active?.state ? { state: r.active.state } : {}),
315
- }));
316
- })
317
- .catch(() => []);
318
- return {
319
- localStorage: Object.fromEntries(localStorageEntries),
320
- sessionStorage: Object.fromEntries(sessionStorageEntries),
321
- cookies: cookies.map((c) => ({
322
- name: c.name,
323
- value: c.value,
324
- domain: c.domain,
325
- path: c.path,
326
- httpOnly: c.httpOnly,
327
- secure: c.secure,
328
- })),
329
- serviceWorkers: workers.map((w) => w.url()),
330
- serviceWorkerRegistrations: registrations,
331
- };
183
+ return inspectStorage(this.page, this.context);
332
184
  }
333
- /**
334
- * Runs one already-resolved Block entry for real - the shared core both
335
- * `applyPick` (resolved via a live-menu index) and `applyPath` (resolved
336
- * directly by name, bypassing live-menu-visibility entirely - see
337
- * `applyPath`'s own comment for why) call. `expectedTo` is only used as a
338
- * fallback when a Block's own `resolve()` doesn't set `__state` - same
339
- * behavior `applyPick` always had, just factored out, not changed.
340
- */
341
185
  /** Paint + log a Block's authored waygraph overlay fixtures (same as demo does), best-effort. */
342
186
  async paintStubPhase(tag, phase, holdMs) {
343
187
  if (!this.page || !stubPhaseHasContent(phase))
@@ -347,6 +191,10 @@ export class AutoSession {
347
191
  verboseLog("pilot", line);
348
192
  await showPilotFixtures(this.page, fixtures).catch(ignorePageGone);
349
193
  }
194
+ /**
195
+ * Runs one Block for real: the core of `applyPick` (picked from the live menu) and `applyPath` (named by the route).
196
+ * `expectedTo` is used when the Block's resolve doesn't set `__state`.
197
+ */
350
198
  async runNamedBlock(entry, expectedTo, instanceOption) {
351
199
  const from = this.here;
352
200
  const stubBeforeResult = await runStubPhase(entry.block, "stubBefore");
@@ -416,13 +264,8 @@ export class AutoSession {
416
264
  return this.runNamedBlock(entry, edge.to, edge.instanceOption);
417
265
  }
418
266
  /**
419
- * Re-discovers the project's Block library and graph from disk, using the
420
- * exact same call `start()` itself makes - not a duplicated read - and
421
- * replaces `this.library`/`this.graph` in place. Leaves the live page,
422
- * mem, browser/context, and current Checkpoint (`here`) untouched, so a
423
- * Block written to disk mid-session becomes pickable via a later
424
- * `applyPick` without restarting the session. See
425
- * openspec/changes/waygraph-blind-pilot/design.md.
267
+ * Re-reads the project's Blocks and graph from disk (the same call `start()` makes) and swaps them in, keeping the
268
+ * page, mem, browser and `here`: a Block written mid-session becomes pickable without a restart.
426
269
  */
427
270
  async reloadLibrary() {
428
271
  if (this.page)
@@ -438,22 +281,8 @@ export class AutoSession {
438
281
  await showPilotActivity(this.page, "Map updated");
439
282
  }
440
283
  /**
441
- * Forces `here` to be re-detected from the real live page, discarding
442
- * whatever was cached - the fix for a real, confirmed gap: `here` is only
443
- * ever updated by an action this class itself ran (`applyPick`/`applyPath`
444
- * set it to a Block's own resolved Checkpoint; `rawClick`/`rawType`/
445
- * `rawGoto` reset it to `null` because *they* just changed the page). If a
446
- * human co-driving the same visible session clicks/types/navigates
447
- * directly - a real scenario for a headful Pilot session someone is
448
- * watching, not hypothetical - none of that goes through this class at
449
- * all, so `here` silently goes stale: `currentMenu()`'s own lazy check
450
- * (`if (this.here === null)`, just above) only re-detects when `here` is
451
- * already unknown, never to confirm a *known* value is still correct.
452
- * This does not crash the session (the process/socket/page all stay
453
- * alive) but subsequent `send`/`reach` calls would act on a menu built
454
- * from the wrong Checkpoint until something forces re-detection - this
455
- * method is that: unconditionally re-run the same `detectHere` logic the
456
- * lazy path already uses, regardless of what `here` currently holds.
284
+ * Re-detects `here` from the live page, whatever is cached. `here` only changes when this session acts; a person
285
+ * clicking in a visible session moves the page without it, and later `send` / `reach` calls would use a stale menu.
457
286
  */
458
287
  async resync() {
459
288
  this.page = await ensureLivePage(this.context, this.page, this.startUrl);
@@ -468,119 +297,70 @@ export class AutoSession {
468
297
  };
469
298
  }
470
299
  /**
471
- * Raw interaction primitives for a page no Block covers yet (Blind Pilot -
472
- * see openspec/changes/waygraph-blind-pilot). Unlike `applyPick`, the
473
- * resulting Checkpoint isn't known ahead of time from a Block's own
474
- * `resolve()`, so each of these invalidates the cached `here` (forcing
475
- * `currentMenu()`'s existing lazy `detectHere` call to run again) rather
476
- * than assuming the page didn't move anywhere a Block now recognizes.
477
- * A selector matching nothing is a reported failure, not a silent no-op.
300
+ * Raw actions for a page no Block covers yet (Blind Pilot). The Checkpoint they land on isn't known ahead, so each
301
+ * one forgets `here` and the next menu re-detects it. A selector that matches nothing is a reported failure.
478
302
  */
479
303
  async rawClick(selector) {
480
- this.page = await ensureLivePage(this.context, this.page, this.startUrl);
481
- await showPilotActivity(this.page, `Running (no Block): click "${selector}"`);
482
- const locator = this.page.locator(selector).first();
483
- if ((await locator.count()) === 0) {
484
- return { ok: false, error: `no element matches selector "${selector}"` };
485
- }
486
- await showPilotVision(this.page, selector, `click: ${selector}`);
487
- await locator.click();
488
- this.here = null;
489
- const menu = await this.currentMenu();
490
- this.lastRunNote = `click "${selector}" -> ${this.here ?? "Unknown"}`;
491
- return {
492
- ok: true,
493
- snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
494
- quit: false,
495
- };
304
+ return this.rawStep(`click "${selector}"`, async (page) => {
305
+ const target = await rawTarget(page, selector, `click: ${selector}`);
306
+ if (typeof target === "string")
307
+ return target;
308
+ await target.click();
309
+ });
496
310
  }
497
311
  async rawType(selector, text) {
498
- this.page = await ensureLivePage(this.context, this.page, this.startUrl);
499
- await showPilotActivity(this.page, `Running (no Block): type into "${selector}"`);
500
- const locator = this.page.locator(selector).first();
501
- if ((await locator.count()) === 0) {
502
- return { ok: false, error: `no element matches selector "${selector}"` };
503
- }
504
- await showPilotVision(this.page, selector, `type: ${selector}`);
505
- await locator.fill(text);
506
- this.here = null;
507
- const menu = await this.currentMenu();
508
- this.lastRunNote = `type into "${selector}" -> ${this.here ?? "Unknown"}`;
509
- return {
510
- ok: true,
511
- snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
512
- quit: false,
513
- };
312
+ return this.rawStep(`type into "${selector}"`, async (page) => {
313
+ const target = await rawTarget(page, selector, `type: ${selector}`);
314
+ if (typeof target === "string")
315
+ return target;
316
+ await target.fill(text);
317
+ });
514
318
  }
515
- /**
516
- * Presses a real key on a real focused element - a real gap found live:
517
- * many real inline-edit inputs (no visible Save button at all) commit on
518
- * Enter, not on blur/click-elsewhere. `key` is a Playwright key name
519
- * (e.g. "Enter", "Escape", "Tab") - same vocabulary as
520
- * `page.keyboard.press`, which this wraps.
521
- */
319
+ /** Presses a key on an element (`Enter`, `Escape`, ...): inline-edit inputs often commit on Enter, with no Save button. */
522
320
  async rawPress(selector, key) {
523
- this.page = await ensureLivePage(this.context, this.page, this.startUrl);
524
- await showPilotActivity(this.page, `Running (no Block): press "${key}" on "${selector}"`);
525
- const locator = this.page.locator(selector).first();
526
- if ((await locator.count()) === 0) {
527
- return { ok: false, error: `no element matches selector "${selector}"` };
528
- }
529
- await showPilotVision(this.page, selector, `press ${key}: ${selector}`);
530
- await locator.press(key);
531
- this.here = null;
532
- const menu = await this.currentMenu();
533
- this.lastRunNote = `press "${key}" on "${selector}" -> ${this.here ?? "Unknown"}`;
534
- return {
535
- ok: true,
536
- snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
537
- quit: false,
538
- };
321
+ return this.rawStep(`press "${key}" on "${selector}"`, async (page) => {
322
+ const target = await rawTarget(page, selector, `press ${key}: ${selector}`);
323
+ if (typeof target === "string")
324
+ return target;
325
+ await target.press(key);
326
+ });
539
327
  }
540
328
  async rawGoto(url) {
541
- this.page = await ensureLivePage(this.context, this.page, this.startUrl);
542
- await showPilotActivity(this.page, `Running (no Block): goto "${url}"`);
543
- try {
544
- await this.page.goto(url, { waitUntil: "domcontentloaded" });
545
- }
546
- catch (err) {
547
- return { ok: false, error: `goto "${url}" failed - ${err instanceof Error ? err.message : String(err)}` };
548
- }
549
- this.here = null;
550
- const menu = await this.currentMenu();
551
- this.lastRunNote = `goto "${url}" -> ${this.here ?? "Unknown"}`;
552
- return {
553
- ok: true,
554
- snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
555
- quit: false,
556
- };
329
+ return this.rawStep(`goto "${url}"`, async (page) => {
330
+ try {
331
+ await page.goto(url, { waitUntil: "domcontentloaded" });
332
+ }
333
+ catch (err) {
334
+ return `goto "${url}" failed - ${err instanceof Error ? err.message : String(err)}`;
335
+ }
336
+ });
557
337
  }
558
- /**
559
- * Fills a real `<input type="file">` - a built-in stub kind
560
- * ("image"/"pdf"/"video", see `stubFilePath`) or a caller-supplied
561
- * `filePath` for anything else ("whatever", per the real request this
562
- * responds to). Same raw-primitive shape as click/type/goto: works with
563
- * zero Blocks, invalidates `here` since the app may react to the upload.
564
- */
338
+ /** Fills an `<input type="file">` with a built-in stub file ("image", "pdf", "video") or a `filePath` of your own. */
565
339
  async rawUpload(selector, stub) {
566
- this.page = await ensureLivePage(this.context, this.page, this.startUrl);
567
340
  const filePath = typeof stub === "string" ? stubFilePath(stub) : stub.filePath;
568
341
  const label = typeof stub === "string" ? stub : filePath;
569
- await showPilotActivity(this.page, `Running (no Block): upload ${label} into "${selector}"`);
570
- const locator = this.page.locator(selector).first();
571
- if ((await locator.count()) === 0) {
572
- return { ok: false, error: `no element matches selector "${selector}"` };
573
- }
574
- await showPilotVision(this.page, selector, `upload ${label}: ${selector}`);
575
- try {
576
- await locator.setInputFiles(filePath);
577
- }
578
- catch (err) {
579
- return { ok: false, error: `upload into "${selector}" failed - ${err instanceof Error ? err.message : String(err)}` };
580
- }
342
+ return this.rawStep(`upload ${label} into "${selector}"`, async (page) => {
343
+ const target = await rawTarget(page, selector, `upload ${label}: ${selector}`);
344
+ if (typeof target === "string")
345
+ return target;
346
+ try {
347
+ await target.setInputFiles(filePath);
348
+ }
349
+ catch (err) {
350
+ return `upload into "${selector}" failed - ${err instanceof Error ? err.message : String(err)}`;
351
+ }
352
+ });
353
+ }
354
+ /** One raw primitive (no Block): announce it, run it, then forget `here` so the next menu re-detects it. */
355
+ async rawStep(what, run) {
356
+ this.page = await ensureLivePage(this.context, this.page, this.startUrl);
357
+ await showPilotActivity(this.page, `Running (no Block): ${what}`);
358
+ const failed = await run(this.page);
359
+ if (failed !== undefined)
360
+ return { ok: false, error: failed };
581
361
  this.here = null;
582
362
  const menu = await this.currentMenu();
583
- this.lastRunNote = `upload ${label} into "${selector}" -> ${this.here ?? "Unknown"}`;
363
+ this.lastRunNote = `${what} -> ${this.here ?? "Unknown"}`;
584
364
  return {
585
365
  ok: true,
586
366
  snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
@@ -594,19 +374,7 @@ export class AutoSession {
594
374
  */
595
375
  async applyHighlight(fixtures) {
596
376
  this.page = await ensureLivePage(this.context, this.page, this.startUrl);
597
- const detail = fixtures.clear === true
598
- ? "clear"
599
- : [
600
- fixtures.rings?.length ? `${fixtures.rings.length} ring(s)` : null,
601
- fixtures.todos?.length ? `${fixtures.todos.length} todo(s)` : null,
602
- fixtures.zoom && fixtures.zoom > 1.001 ? `zoom ${fixtures.zoom}` : null,
603
- fixtures.device
604
- ? `device ${typeof fixtures.device === "string" ? fixtures.device : fixtures.device.preset || "custom"}`
605
- : null,
606
- fixtures.rings?.some((r) => r.focus) ? "focus" : null,
607
- ]
608
- .filter(Boolean)
609
- .join(", ") || "fixtures";
377
+ const detail = describeHighlight(fixtures);
610
378
  // Same bottom-left activity toast as Running (Dom)/(Block) - agent narration.
611
379
  await showPilotActivity(this.page, `Highlighting: ${detail}`);
612
380
  try {
@@ -633,42 +401,11 @@ export class AutoSession {
633
401
  }
634
402
  }
635
403
  /**
636
- * Runs a whole multi-step route to `targetCheckpoint` in one call, instead
637
- * of an agent hand-picking one index at a time across many separate
638
- * `applyPick` round trips - real, reported pain for any non-trivial task
639
- * on a rich graph ("the entire prompts of the day" driving one step per
640
- * call).
641
- *
642
- * Two other designs were tried and rejected by direct reproduction against
643
- * live saucedemo.com before this one, both undone by the same root cause:
644
- * a `from: "*"` edge (e.g. "Checkout", only clickable once you're actually
645
- * on the cart page with items) looks globally reachable to
646
- * `findBlockPath`'s static-graph BFS, which has no live-page access and
647
- * can't know the edge's real precondition isn't showing yet. Computing the
648
- * whole route once upfront names a premature wildcard step immediately;
649
- * scoring each live menu option by its static-graph distance to the
650
- * target (greedy best-first) doesn't fix it either - a harmless self-loop
651
- * Method can score just as well as real progress once wildcard "shortcuts"
652
- * are baked into every distance calculation, and the session gets stuck
653
- * repeating it.
654
- *
655
- * The fix that actually works: stop trying to be clever about live-menu
656
- * visibility per step, and reuse `auto --blocks <From> <To>`'s own already
657
- * -proven approach instead - compute the path ONCE (via
658
- * `findBlockPathDetailed`, the same BFS `findBlockPath`/`runChainAuto`
659
- * already use, extended to also return each hop's specific edge - see its
660
- * own doc comment for why that extra detail is load-bearing, not cosmetic),
661
- * then run each Block in it directly via `runNamedBlock`, trusting the
662
- * graph the same way `runChain`'s own flow execution already does, not
663
- * gated on live-menu visibility at all. The one real difference from
664
- * `runChainAuto`: this runs against the session's own already-live
665
- * page/context/mem, not a freshly spawned browser - the whole point of
666
- * staying in one session. A step whose real precondition genuinely isn't
667
- * met still fails loud, naming that step, exactly the way a mis-ordered
668
- * `runChain` spec would - and a step that runs without throwing but lands
669
- * somewhere other than what its own edge promised (a real, observed case,
670
- * not hypothetical - see this method's own test suite) fails loud too,
671
- * rather than reporting false success.
404
+ * Runs a whole route to `targetCheckpoint` in one call, instead of one `applyPick` per step. The path is computed
405
+ * once with `findBlockPathDetailed` (the BFS `auto --blocks <From> <To>` uses) and each hop's Block runs directly on
406
+ * the session's live page and mem, trusting the graph rather than live-menu visibility. Per-step choosing was tried
407
+ * and fails: a `from: "*"` edge looks reachable from anywhere to a static BFS. A step whose precondition isn't met
408
+ * fails loud, naming it; so does a step that runs but lands somewhere other than its edge promised.
672
409
  */
673
410
  async applyPath(targetCheckpoint) {
674
411
  await this.currentMenu();
@@ -697,15 +434,8 @@ export class AutoSession {
697
434
  if (!result.ok) {
698
435
  return { ok: false, error: `step "${step.block}" failed - ${result.error}` };
699
436
  }
700
- // A Block can resolve to a DIFFERENT real Checkpoint than this exact
701
- // hop's own edge expected, without throwing at all - e.g.
702
- // submit-login's own resolve() legitimately "stays on LoginPage" on
703
- // bad auth instead of erroring. Trusting `result.ok` alone would
704
- // silently report success while sitting on the wrong page - a real
705
- // bug caught by this method's own test suite, not theoretical.
706
- // `step.to` (not a name-based re-lookup - a union-Out Block can have
707
- // several edges sharing one name with different `to` tags) is exactly
708
- // the Checkpoint THIS hop's own edge in the computed route promised.
437
+ // A Block can land somewhere else without throwing (submit-login stays on LoginPage on bad auth): compare with
438
+ // what this hop's own edge promised (`step.to`; one name can have several edges).
709
439
  if (this.here !== step.to) {
710
440
  return {
711
441
  ok: false,
@@ -0,0 +1,4 @@
1
+ import type { Page } from "@playwright/test";
2
+ import type { ConsoleLogEntry } from "./helpers.js";
3
+ /** Records what the page logs into `push` for `auto console`. */
4
+ export declare function recordPageConsole(page: Page, push: (entry: ConsoleLogEntry) => void): void;
@@ -0,0 +1,41 @@
1
+ /** Records what the page logs into `push` for `auto console`. */
2
+ export function recordPageConsole(page, push) {
3
+ // Attached once, on the page created here - `ensureLivePage` only swaps
4
+ // to a fresh Page if the current one closed (a rare case, e.g. the human
5
+ // closing the tab), not on ordinary same-page SPA navigation, which is
6
+ // all this target (and most real sites) actually do.
7
+ page.on("console", (msg) => {
8
+ push({
9
+ type: "console",
10
+ level: msg.type(),
11
+ text: msg.text(),
12
+ timestamp: new Date().toISOString(),
13
+ });
14
+ });
15
+ page.on("pageerror", (err) => {
16
+ push({
17
+ type: "pageerror",
18
+ text: err instanceof Error ? err.message : String(err),
19
+ timestamp: new Date().toISOString(),
20
+ });
21
+ });
22
+ page.on("response", (res) => {
23
+ // Every failed response (the original case), PLUS every mutation
24
+ // (non-GET) response regardless of status - a real gap found live:
25
+ // "auto console" only logging failures made a SUCCESSFUL POST that
26
+ // simply didn't navigate anywhere look identical to "nothing
27
+ // happened at all," undiagnosable the same way a real failure was.
28
+ // A mutation's outcome (2xx or not) is always diagnostically
29
+ // relevant; a GET's usually isn't (assets, trackers) unless it failed.
30
+ const method = res.request().method();
31
+ if (res.status() < 400 && method === "GET")
32
+ return;
33
+ push({
34
+ type: "response",
35
+ text: `${method} ${res.statusText()}`,
36
+ url: res.url(),
37
+ status: res.status(),
38
+ timestamp: new Date().toISOString(),
39
+ });
40
+ });
41
+ }