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.
- package/dist/cli/commands/browser.d.ts +2 -0
- package/dist/cli/commands/browser.js +109 -0
- package/dist/cli/commands/codemod-stubs.js +90 -2
- package/dist/cli/commands/pilot.d.ts +2 -0
- package/dist/cli/commands/pilot.js +102 -0
- package/dist/cli/commands/session-ops.d.ts +3 -0
- package/dist/cli/commands/session-ops.js +257 -0
- package/dist/cli/commands/session.js +20 -461
- package/dist/cli/run-chain.js +2 -2
- package/dist/cli/usage.d.ts +1 -1
- package/dist/cli/util.js +1 -1
- package/dist/demo/narration/demo-pace.d.ts +2 -2
- package/dist/demo/narration/demo-pace.js +1 -1
- package/dist/demo/narration/device.d.ts +2 -2
- package/dist/demo/narration/merge.d.ts +5 -3
- package/dist/demo/narration/merge.js +3 -7
- package/dist/demo/narration/narration.d.ts +9 -13
- package/dist/demo/narration/narration.js +5 -17
- package/dist/demo/narration/stub-ctx.js +3 -3
- package/dist/demo/narration/todo-dock.d.ts +5 -5
- package/dist/demo/overlay/cursor.d.ts +9 -2
- package/dist/demo/overlay/cursor.js +1 -1
- package/dist/demo/overlay/device-stage.d.ts +15 -2
- package/dist/demo/overlay/inpage/banner.d.ts +2 -1
- package/dist/demo/overlay/inpage/banner.js +3 -3
- package/dist/demo/overlay/inpage/caption.d.ts +1 -1
- package/dist/demo/overlay/inpage/core.d.ts +2 -1
- package/dist/demo/overlay/inpage/core.js +1 -7
- package/dist/demo/overlay/inpage/device-chip.d.ts +1 -1
- package/dist/demo/overlay/inpage/device-chip.js +6 -6
- package/dist/demo/overlay/inpage/network.d.ts +3 -2
- package/dist/demo/overlay/inpage/network.js +1 -12
- package/dist/demo/overlay/inpage/panel-chrome.d.ts +1 -1
- package/dist/demo/overlay/inpage/panel-chrome.js +1 -1
- package/dist/demo/overlay/inpage/stamp.d.ts +1 -1
- package/dist/demo/overlay/inpage/stamp.js +1 -1
- package/dist/demo/overlay/inpage/swipe-zoom.d.ts +1 -1
- package/dist/demo/overlay/inpage/swipe-zoom.js +1 -1
- package/dist/demo/overlay/inpage/todos.d.ts +2 -1
- package/dist/demo/overlay/inpage/todos.js +1 -2
- package/dist/demo/overlay/network-capture.d.ts +2 -2
- package/dist/demo/overlay/network-capture.js +1 -1
- package/dist/demo/overlay/overlay-css.js +1 -55
- package/dist/demo/overlay/overlay-stage.d.ts +19 -11
- package/dist/demo/overlay/overlay.d.ts +2 -2
- package/dist/demo/overlay/overlay.js +3 -2
- package/dist/demo/overlay/rings.d.ts +38 -18
- package/dist/demo/overlay/rings.js +21 -62
- package/dist/demo/overlay/ui/overlay.css +40 -0
- package/dist/demo/stepper/after.js +11 -34
- package/dist/demo/stepper/banner-log.d.ts +4 -6
- package/dist/demo/stepper/banner-log.js +3 -1
- package/dist/demo/stepper/before.js +6 -4
- package/dist/demo/stepper/demo-log.d.ts +26 -9
- package/dist/demo/stepper/demo-log.js +10 -27
- package/dist/demo/stepper/interaction-patch.d.ts +3 -2
- package/dist/demo/stepper/interaction-patch.js +22 -34
- package/dist/demo/stepper/panel-after.d.ts +17 -0
- package/dist/demo/stepper/panel-after.js +166 -0
- package/dist/demo/stepper/panel-before.d.ts +12 -0
- package/dist/demo/stepper/panel-before.js +241 -0
- package/dist/demo/stepper/slides.d.ts +16 -1
- package/dist/demo/stepper/slides.js +21 -5
- package/dist/demo/stepper/step-panels.d.ts +57 -9
- package/dist/demo/stepper/step-panels.js +10 -427
- package/dist/demo/stepper/stepper.js +1 -1
- package/dist/demo/stepper/teardown.d.ts +4 -5
- package/dist/demo/stepper/teardown.js +4 -6
- package/dist/demo/stepper/todo-advance.d.ts +3 -2
- package/dist/demo/stepper/todo-advance.js +2 -2
- package/dist/demo/stepper/view.d.ts +4 -14
- package/dist/demo/types.d.ts +69 -1
- package/dist/demo/verbose-log.js +1 -1
- package/dist/engine/block/action-page.d.ts +53 -0
- package/dist/engine/block/action-page.js +1 -0
- package/dist/engine/block/block.d.ts +24 -90
- package/dist/engine/block/block.js +2 -77
- package/dist/engine/block/branch.d.ts +17 -0
- package/dist/engine/block/branch.js +40 -0
- package/dist/engine/block/dialog.d.ts +13 -0
- package/dist/engine/block/dialog.js +37 -0
- package/dist/engine/block/kinds/method.d.ts +0 -2
- package/dist/engine/block/kinds/method.js +0 -1
- package/dist/engine/block/kinds/nav.js +1 -1
- package/dist/engine/flow/map-branch.d.ts +9 -0
- package/dist/engine/flow/map-branch.js +57 -0
- package/dist/engine/flow/map-builder.d.ts +7 -22
- package/dist/engine/flow/map-builder.js +6 -145
- package/dist/engine/flow/map-steps.d.ts +30 -0
- package/dist/engine/flow/map-steps.js +87 -0
- package/dist/engine/run/run.d.ts +1 -19
- package/dist/engine/run/run.js +0 -88
- package/dist/engine/stub/ring.d.ts +2 -4
- package/dist/engine/stub/ring.js +1 -0
- package/dist/engine/stub/stub.d.ts +16 -8
- package/dist/engine/trait/trait.js +1 -1
- package/dist/index.d.ts +15 -12
- package/dist/index.js +9 -7
- package/dist/live/auto/auto.d.ts +3 -15
- package/dist/live/auto/auto.js +10 -420
- package/dist/live/auto/explore.d.ts +2 -2
- package/dist/live/auto/mem.d.ts +11 -0
- package/dist/live/auto/mem.js +85 -0
- package/dist/live/auto/menu.d.ts +30 -0
- package/dist/live/auto/menu.js +335 -0
- package/dist/live/auto-session/auto-session.d.ts +23 -103
- package/dist/live/auto-session/auto-session.js +100 -370
- package/dist/live/auto-session/console.d.ts +4 -0
- package/dist/live/auto-session/console.js +41 -0
- package/dist/live/auto-session/files.d.ts +44 -0
- package/dist/live/auto-session/files.js +145 -0
- package/dist/live/auto-session/helpers.d.ts +5 -2
- package/dist/live/auto-session/helpers.js +17 -0
- package/dist/live/auto-session/inspect.d.ts +4 -0
- package/dist/live/auto-session/inspect.js +75 -0
- package/dist/live/auto-session/ipc.d.ts +3 -22
- package/dist/live/auto-session/ipc.js +7 -147
- package/dist/live/auto-session/open-browser.d.ts +6 -0
- package/dist/live/auto-session/open-browser.js +47 -0
- package/dist/live/pilot/pilot.d.ts +1 -1
- package/dist/live/pilot/pilot.js +2 -1
- package/dist/live/traverse/traverse.js +6 -200
- package/dist/live/traverse/worker.d.ts +37 -0
- package/dist/live/traverse/worker.js +200 -0
- package/dist/runner/plan/find.js +2 -2
- package/dist/runner/plan/plan.js +1 -1
- package/dist/runner/plan/seed-mem.js +5 -5
- package/dist/runner/plan/spec.js +1 -1
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
- package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +2 -1
- package/examples/saucedemo/src/flows/checkout.flow.ts +2 -1
- package/examples/saucedemo/src/flows/shop.flow.ts +2 -1
- package/examples/saucedemo/src/stubs/device-showcase.ts +37 -43
- package/examples/saucedemo/src/stubs/finish-order.ts +38 -0
- package/package.json +5 -1
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
- package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +2 -1
- package/templates/quickstart/src/flows/checkout.flow.ts +2 -1
- package/templates/quickstart/src/flows/shop.flow.ts +2 -1
- package/templates/quickstart/src/stubs/device-showcase.ts +37 -43
- package/templates/quickstart/src/stubs/finish-order.ts +38 -0
- package/templates/scaffold/src/flows/home/hello.flow.ts +3 -0
- 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 {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
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
|
-
*
|
|
21
|
-
* `
|
|
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 {
|
|
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
|
-
|
|
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
|
|
187
|
-
// the
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
420
|
-
*
|
|
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
|
-
*
|
|
442
|
-
*
|
|
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
|
|
472
|
-
*
|
|
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.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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.
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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 =
|
|
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
|
|
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
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
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
|
|
701
|
-
// hop's own edge
|
|
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,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
|
+
}
|