waygraph 0.16.2 → 0.16.4

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 (123) hide show
  1. package/README.md +12 -0
  2. package/dist/analysis/practices-check.d.ts +1 -1
  3. package/dist/analysis/practices-check.js +36 -0
  4. package/dist/beta.d.ts +6 -0
  5. package/dist/beta.js +5 -0
  6. package/dist/cli/commands/codemod-flows.js +2 -2
  7. package/dist/cli/commands/codemod-stubs.js +43 -1
  8. package/dist/cli/commands/pilot.js +3 -16
  9. package/dist/cli/commands/session-ops.js +26 -1
  10. package/dist/cli/flags.d.ts +1 -1
  11. package/dist/cli/flags.js +1 -1
  12. package/dist/cli/usage.js +6 -0
  13. package/dist/demo/narration/stub-ctx.js +126 -124
  14. package/dist/demo/overlay/cursor.js +1 -1
  15. package/dist/demo/overlay/inpage/banner.js +5 -0
  16. package/dist/demo/overlay/inpage/core.js +1 -0
  17. package/dist/demo/overlay/inpage/network.js +1 -1
  18. package/dist/demo/overlay/inpage/todos.js +0 -15
  19. package/dist/demo/overlay/network-capture.js +2 -2
  20. package/dist/demo/overlay/overlay-css.js +18 -21
  21. package/dist/demo/overlay/overlay-stage.js +1 -1
  22. package/dist/demo/overlay/phase-fixtures.js +5 -5
  23. package/dist/demo/overlay/pilot/shell.js +7 -5
  24. package/dist/demo/overlay/ui/components/ring.js +2 -2
  25. package/dist/demo/overlay/ui/overlay.css +98 -35
  26. package/dist/demo/overlay/ui/shadow.js +69 -2
  27. package/dist/demo/stepper/demo-log.js +8 -8
  28. package/dist/demo/stepper/slides.d.ts +1 -1
  29. package/dist/demo/stepper/slides.js +1 -1
  30. package/dist/demo/stepper/stepper.js +1 -1
  31. package/dist/engine/block/kind.d.ts +3 -0
  32. package/dist/engine/block/kind.js +11 -0
  33. package/dist/engine/errors.d.ts +12 -0
  34. package/dist/engine/errors.js +12 -0
  35. package/dist/engine/read/read-block.d.ts +61 -0
  36. package/dist/engine/read/read-block.js +94 -0
  37. package/dist/engine/read/read-key.d.ts +32 -0
  38. package/dist/engine/read/read-key.js +34 -0
  39. package/dist/engine/read/read-page.d.ts +19 -0
  40. package/dist/engine/read/read-page.js +50 -0
  41. package/dist/engine/read/readers.d.ts +82 -0
  42. package/dist/engine/read/readers.js +43 -0
  43. package/dist/engine/read/run-read.d.ts +17 -0
  44. package/dist/engine/read/run-read.js +156 -0
  45. package/dist/engine/run/connect.js +3 -2
  46. package/dist/engine/run/events.d.ts +28 -10
  47. package/dist/engine/run/events.js +52 -38
  48. package/dist/engine/run/run.js +25 -13
  49. package/dist/engine/stub/stub.d.ts +26 -21
  50. package/dist/index.d.ts +7 -4
  51. package/dist/index.js +9 -4
  52. package/dist/live/auto/auto.js +2 -3
  53. package/dist/live/auto/menu.js +3 -2
  54. package/dist/live/auto-session/auto-session.d.ts +9 -2
  55. package/dist/live/auto-session/auto-session.js +23 -20
  56. package/dist/live/auto-session/feed.d.ts +16 -0
  57. package/dist/live/auto-session/feed.js +71 -0
  58. package/dist/live/auto-session/helpers.d.ts +7 -0
  59. package/dist/live/auto-session/helpers.js +9 -0
  60. package/dist/live/auto-session/ipc.d.ts +15 -47
  61. package/dist/live/auto-session/ipc.js +34 -4
  62. package/dist/live/auto-session/protocol.d.ts +127 -0
  63. package/dist/live/auto-session/protocol.js +6 -0
  64. package/dist/live/auto-session/read.d.ts +25 -0
  65. package/dist/live/auto-session/read.js +50 -0
  66. package/dist/live/traverse/worker.d.ts +1 -0
  67. package/dist/live/traverse/worker.js +6 -1
  68. package/dist/manifest/manifest.d.ts +22 -2
  69. package/dist/manifest/manifest.js +27 -1
  70. package/dist/manifest/project.d.ts +10 -1
  71. package/dist/manifest/project.js +9 -12
  72. package/dist/manifest/read-blocks.d.ts +2 -0
  73. package/dist/manifest/read-blocks.js +23 -0
  74. package/dist/manifest/scan.d.ts +3 -2
  75. package/dist/manifest/scan.js +8 -4
  76. package/dist/runner/runner.d.ts +0 -1
  77. package/dist/runner/runner.js +6 -6
  78. package/dist/runtime/full-launch.d.ts +2 -1
  79. package/dist/runtime/full-launch.js +29 -27
  80. package/dist/runtime/run-log.d.ts +2 -0
  81. package/dist/runtime/run-log.js +13 -6
  82. package/examples/saucedemo/README.md +2 -2
  83. package/examples/saucedemo/src/blocks/SITE-MAP.md +3 -0
  84. package/examples/saucedemo/src/blocks/saucedemo-web/cart/cart.read.block.ts +26 -0
  85. package/examples/saucedemo/src/blocks/saucedemo-web/header.read.block.ts +25 -0
  86. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.read.block.ts +28 -0
  87. package/examples/saucedemo/src/flows/api-demo.flow.ts +1 -1
  88. package/examples/saucedemo/src/flows/cart-bulk.flow.ts +1 -1
  89. package/examples/saucedemo/src/flows/cart-continue.flow.ts +1 -1
  90. package/examples/saucedemo/src/flows/catalog-logout.flow.ts +1 -1
  91. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +1 -1
  92. package/examples/saucedemo/src/flows/checkout.flow.ts +11 -11
  93. package/examples/saucedemo/src/flows/login.flow.ts +1 -1
  94. package/examples/saucedemo/src/flows/shop.flow.ts +1 -1
  95. package/examples/saucedemo/src/flows/stub-on-error.flow.ts +1 -1
  96. package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +1 -1
  97. package/examples/saucedemo/src/states/checkout.reads.ts +9 -0
  98. package/examples/saucedemo/src/stubs/device-showcase.ts +37 -37
  99. package/examples/saucedemo/src/stubs/finish-order.ts +2 -2
  100. package/examples/saucedemo/src/stubs/login.ts +5 -5
  101. package/package.json +5 -1
  102. package/templates/quickstart/src/flows/api-demo.flow.ts +1 -1
  103. package/templates/quickstart/src/flows/cart-bulk.flow.ts +1 -1
  104. package/templates/quickstart/src/flows/cart-continue.flow.ts +1 -1
  105. package/templates/quickstart/src/flows/catalog-logout.flow.ts +1 -1
  106. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +1 -1
  107. package/templates/quickstart/src/flows/checkout.flow.ts +11 -11
  108. package/templates/quickstart/src/flows/login.flow.ts +1 -1
  109. package/templates/quickstart/src/flows/shop.flow.ts +1 -1
  110. package/templates/quickstart/src/flows/stub-on-error.flow.ts +1 -1
  111. package/templates/quickstart/src/flows/viewer-blocked.flow.ts +1 -1
  112. package/templates/quickstart/src/stubs/device-showcase.ts +37 -37
  113. package/templates/quickstart/src/stubs/finish-order.ts +2 -2
  114. package/templates/quickstart/src/stubs/login.ts +5 -5
  115. package/templates/scaffold/src/flows/(external)/hoppscotch/auth.flow.ts +1 -1
  116. package/templates/scaffold/src/flows/(external)/hoppscotch/get.flow.ts +1 -1
  117. package/templates/scaffold/src/flows/(external)/hoppscotch/post.flow.ts +1 -1
  118. package/templates/scaffold/src/flows/(user-stories)/email-verification.flow.ts +4 -4
  119. package/templates/scaffold/src/flows/README.md +1 -1
  120. package/templates/scaffold/src/flows/home/cart.flow.ts +7 -7
  121. package/templates/scaffold/src/flows/home/hello.flow.ts +8 -8
  122. package/templates/skills/waygraph-convention.skill.md +10 -2
  123. package/templates/skills/waygraph-pilot.skill.md +7 -0
package/README.md CHANGED
@@ -100,6 +100,7 @@ names) still works and is fine for small projects, but the Map is where the tool
100
100
  | See the whole graph | `waygraph graph` |
101
101
  | Lint your Blocks and folders | `waygraph check` / `waygraph typecheck` / `waygraph map` |
102
102
  | Open a persistent browser for an agent | `waygraph browser start` |
103
+ | Read what the page shows, as JSON (beta: Read Blocks) | `waygraph pilot read <id> [path]` |
103
104
  | Highlight things on the live page | `waygraph browser highlight <id> "#login\|Login button\|warning"` |
104
105
  | Set up coding-agent skills | `waygraph agent-dive --loop claude` |
105
106
 
@@ -124,4 +125,15 @@ npm run typecheck && npm test # unit + Playwright suites
124
125
  Working on waygraph: [`AGENTS.md`](./AGENTS.md) (the rules, how to verify) and
125
126
  [`src/ARCHITECTURE.md`](./src/ARCHITECTURE.md) (where the code starts).
126
127
 
128
+ ## Status: alpha
129
+
130
+ > [!WARNING]
131
+ > Waygraph is an alpha build. Its syntax may still change in major ways between releases, and a release can remove
132
+ > API (each removal is listed in [what was removed](./docs/deprecated.md); `waygraph upgrade --codemod` converts most
133
+ > of it). For now we recommend staying on 0.16.0:
134
+ >
135
+ > ```bash
136
+ > npm install --save-exact waygraph@0.16.0
137
+ > ```
138
+
127
139
  MIT
@@ -1,4 +1,4 @@
1
- export type PracticeKind = "multi-input" | "combined-action" | "empty-verify" | "overcomplex-selector" | "inline-selector-should-use-sel-file";
1
+ export type PracticeKind = "multi-input" | "combined-action" | "empty-verify" | "overcomplex-selector" | "inline-selector-should-use-sel-file" | "hand-written-ui";
2
2
  export interface PracticeWarning {
3
3
  kind: PracticeKind;
4
4
  file: string;
@@ -46,6 +46,15 @@ const XPATH_LOCATOR = /\.locator\(\s*["'`](?:xpath=)?\/\//;
46
46
  * scoped to simple, single-token selectors (id/class/attr/tag) so this doesn't fire on the
47
47
  * legitimate multi-part selectors `.sel.ts` files themselves are made of. */
48
48
  const INLINE_SIMPLE_SELECTOR = /(?:\.locator|\bclick|\burl)\s*[:(]\s*["'`](#|\.)[\w-]+["'`]/;
49
+ /**
50
+ * Hand-written UI in a test project - real, recurring pattern: an LLM wanting a caption, a highlight or a banner builds
51
+ * its own DOM in the page or restyles / reaches into waygraph's overlay, instead of using `ctx.ui`. It changes the page
52
+ * under test and breaks whenever the overlay changes.
53
+ */
54
+ const OVERLAY_INTERNALS = /\b__wg[A-Za-z]|["'`][^"'`\n]*(?:#wg-|\.wg-|wg-root)/;
55
+ const DOM_BUILD = /\bdocument\.createElement\(|\.innerHTML\s*=|\binsertAdjacentHTML\(|\baddStyleTag\(|\.style\.cssText|\.style\.[a-zA-Z]+\s*=(?!=)/;
56
+ /** Shared narration (`src/stubs/*.ts`) is checked for hand-written UI too, next to Block and flow files. */
57
+ const STUB_FILE = /[\\/]stubs[\\/][^\\/]+\.ts$/;
49
58
  const METHOD_OR_EFFECT_FILE = /\.(method|effect)\.block\.ts$/;
50
59
  const METHOD_OR_EFFECT_DEFINE = /\bdefine(?:Method|Effect)Block\b/;
51
60
  /** Whole-file opt-out: `// waygraph-ignore-practices` */
@@ -55,6 +64,13 @@ const IGNORE_KINDS = /waygraph-ignore:\s*([^\n*]+)/;
55
64
  function walkBlockAndFlowFiles(projectDir, walkDir) {
56
65
  return walkDir(projectDir, SCAN_GLOB);
57
66
  }
67
+ function handWrittenUi(src) {
68
+ if (OVERLAY_INTERNALS.test(src))
69
+ return "reaches into waygraph's overlay (#wg-*, .wg-*, window.__wg*)";
70
+ if (DOM_BUILD.test(src))
71
+ return "builds or restyles page DOM (createElement / innerHTML / addStyleTag / .style)";
72
+ return undefined;
73
+ }
58
74
  function ignoredKindsForFile(src) {
59
75
  if (IGNORE_ALL.test(src))
60
76
  return "all";
@@ -143,6 +159,9 @@ export function collectPracticeWarnings(projectDir, walkDir, opts) {
143
159
  });
144
160
  }
145
161
  }
162
+ const ui = isIgnored("hand-written-ui", ignore) ? undefined : handWrittenUi(src);
163
+ if (ui)
164
+ warnings.push(handWrittenUiWarning(rel, ui));
146
165
  if (!isIgnored("empty-verify", ignore) && IS_ASSERT_BLOCK.test(src) && EMPTY_VERIFY_ARRAY.test(src)) {
147
166
  warnings.push({
148
167
  kind: "empty-verify",
@@ -152,8 +171,23 @@ export function collectPracticeWarnings(projectDir, walkDir, opts) {
152
171
  });
153
172
  }
154
173
  }
174
+ for (const file of walkDir(projectDir, /\.ts$/).filter((f) => STUB_FILE.test(f) && !SCAN_GLOB.test(f))) {
175
+ const src = readFileSync(file, "utf-8");
176
+ const ui = isIgnored("hand-written-ui", ignoredKindsForFile(src)) ? undefined : handWrittenUi(src);
177
+ if (ui)
178
+ warnings.push(handWrittenUiWarning(relative(projectDir, file).replace(/\\/g, "/"), ui));
179
+ }
155
180
  return warnings;
156
181
  }
182
+ function handWrittenUiWarning(file, what) {
183
+ return {
184
+ kind: "hand-written-ui",
185
+ file,
186
+ detail: `${what} - narration goes through ctx.ui (ring, caption, title, todos, slides, device, zoom, network tab), ` +
187
+ "and a test never changes the page it tests. Missing something in ctx.ui? Ask for it in waygraph instead of " +
188
+ "overriding; opt out with // waygraph-ignore: hand-written-ui",
189
+ };
190
+ }
157
191
  export function practiceKindLabel(kind) {
158
192
  switch (kind) {
159
193
  case "multi-input":
@@ -166,6 +200,8 @@ export function practiceKindLabel(kind) {
166
200
  return "regex/XPath selector where a plain one would do";
167
201
  case "inline-selector-should-use-sel-file":
168
202
  return "inline selector — a _sel.ts/*.sel.ts file already exists here";
203
+ case "hand-written-ui":
204
+ return "hand-written UI — use ctx.ui, don't build or override the overlay";
169
205
  default: {
170
206
  const _exhaustive = kind;
171
207
  return _exhaustive;
package/dist/beta.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { reads, ReadKey } from "./engine/read/read-key.js";
2
+ export type { ReadNamespace, ReadSpec, ReadFieldsOf } from "./engine/read/read-key.js";
3
+ export type { Reader, ReadCtx, RowCtx } from "./engine/read/readers.js";
4
+ export { ReadPage } from "./engine/read/read-page.js";
5
+ export { defineReadBlock, readCheckpoint } from "./engine/read/read-block.js";
6
+ export type { ReadBlock, ReadBlockOptions, CheckpointRead } from "./engine/read/read-block.js";
package/dist/beta.js ADDED
@@ -0,0 +1,5 @@
1
+ // The `waygraph/beta` entry: features whose shape may still change before they move into `waygraph`. Import them from
2
+ // "waygraph/beta"; a beta release can rename or reshape them.
3
+ export { reads, ReadKey } from "./engine/read/read-key.js";
4
+ export { ReadPage } from "./engine/read/read-page.js";
5
+ export { defineReadBlock, readCheckpoint } from "./engine/read/read-block.js";
@@ -103,7 +103,7 @@ function indentOf(node) {
103
103
  return " ".repeat(statement.getIndentationLevel() * 2);
104
104
  }
105
105
  function titleStub(block, title, indent) {
106
- return `${block}.stubBefore((ctx) => {\n${indent} ctx.title(${title});\n${indent} })`;
106
+ return `${block}.stubBefore((ctx) => {\n${indent} ctx.ui.title(${title});\n${indent} })`;
107
107
  }
108
108
  function chain(head, steps, indent) {
109
109
  return [head, ...steps, ".end()"].join(`\n${indent} `);
@@ -172,7 +172,7 @@ function rewrite(call, kindOf, needsMap) {
172
172
  if (name === "withTitle") {
173
173
  const first = mapCall && firstStepOf(mapCall);
174
174
  if (!first)
175
- return `withTitle(${flow.getText().split("\n")[0]}, …): name the flow with ctx.title(…) in a stub on its first step`;
175
+ return `withTitle(${flow.getText().split("\n")[0]}, …): name the flow with ctx.ui.title(…) in a stub on its first step`;
176
176
  addTitle(first, args[1].getText());
177
177
  }
178
178
  else if (mapCall) {
@@ -6,7 +6,8 @@
6
6
  // `ctx.ring` calls, and a stub set in a `define*Block({ ... })` (options or instruction) moves out to a chained
7
7
  // `.stubBefore(...)` on the Block; a Block's `slides` become `ctx.slides(...)` in its chained `.stubAfter(...)`;
8
8
  // `ctx.highlights({ a: ring })` becomes one `ctx.ring("a", ring)` per ring; `narrate(locator, caption, action)` in an
9
- // `act` becomes the action, reported so the caption can move to a stubBefore ring. Works on the TypeScript AST (ts-morph).
9
+ // `act` becomes the action, reported so the caption can move to a stubBefore ring. Last, everything a stub paints moves
10
+ // under `ctx.ui` (`ctx.ring(...)` -> `ctx.ui.ring(...)`). Works on the TypeScript AST (ts-morph).
10
11
  import { Node, SyntaxKind } from "ts-morph";
11
12
  const RENAMED = {
12
13
  banner: (a) => `title(${a.join(", ")})`,
@@ -248,6 +249,45 @@ function unwrapNarrate(sf, problems) {
248
249
  }
249
250
  return changed;
250
251
  }
252
+ /** Everything a stub paints, which 0.16.3 moved under `ctx.ui`. */
253
+ const UI_METHODS = new Set(["ring", "todos", "todoIndex", "todoGroups", "hideTodos", "device", "viewport", "touch", "orientation", "hideDevice", "zoom", "zoomOut", "title", "titlePos", "bannerUi", "caption", "hideCaption", "slides", "hideBanner", "showNetworkTab", "hideNetworkTab", "todoDockUi"]);
254
+ /** Whether `name` at `node` is a stub's ctx: the nearest enclosing function with a parameter of that name is a stub. */
255
+ function isStubCtxName(node, name) {
256
+ for (const fn of node.getAncestors()) {
257
+ if (!(Node.isArrowFunction(fn) || Node.isFunctionExpression(fn) || Node.isFunctionDeclaration(fn) || Node.isMethodDeclaration(fn)))
258
+ continue;
259
+ const param = fn.getParameters().find((p) => p.getName() === name);
260
+ if (!param)
261
+ continue;
262
+ if (/StubCtx/.test(param.getType().getText(param)))
263
+ return true;
264
+ const parent = fn.getParent();
265
+ const callee = Node.isCallExpression(parent) ? parent.getExpression() : undefined;
266
+ if (Node.isPropertyAccessExpression(callee) && /^stub(Before|After|OnError)$/.test(callee.getName()))
267
+ return true;
268
+ if (Node.isPropertyAssignment(parent) && /^stub(Before|After|OnError)$/.test(parent.getName()))
269
+ return true;
270
+ // `((ctx) => ...)(ctx)`, as the slides merge writes it: called at once with an outer stub's ctx.
271
+ const call = Node.isParenthesizedExpression(parent) ? parent.getParent() : undefined;
272
+ const arg = Node.isCallExpression(call) ? call.getArguments()[0] : undefined;
273
+ return arg !== undefined && Node.isIdentifier(arg) && isStubCtxName(call, arg.getText());
274
+ }
275
+ return false;
276
+ }
277
+ /** `ctx.ring(...)` -> `ctx.ui.ring(...)`, everywhere a stub's ctx paints (idempotent: `ctx.ui.ring` is left alone). */
278
+ function moveToUi(sf) {
279
+ let changed = false;
280
+ for (const access of sf.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression).reverse()) {
281
+ if (access.wasForgotten() || !UI_METHODS.has(access.getName()))
282
+ continue;
283
+ const receiver = access.getExpression();
284
+ if (!Node.isIdentifier(receiver) || !isStubCtxName(access, receiver.getText()))
285
+ continue;
286
+ access.replaceWithText(`${receiver.getText()}.ui.${access.getName()}`);
287
+ changed = true;
288
+ }
289
+ return changed;
290
+ }
251
291
  /** Rewrites the old stub forms in every file; returns the files it changed and the calls left for a person. */
252
292
  export function convertStubs(project) {
253
293
  const changed = [];
@@ -267,6 +307,8 @@ export function convertStubs(project) {
267
307
  if (convertStatements(block.getStatements(), left))
268
308
  touched = true;
269
309
  }
310
+ if (moveToUi(sf))
311
+ touched = true;
270
312
  if (touched)
271
313
  changed.push(sf);
272
314
  problems.push(...left.map((p) => ({ file: sf.getFilePath(), ...p })));
@@ -9,6 +9,7 @@ import { resolveBaseUrl } from "../../runtime/waygraph-config.js";
9
9
  import { parseCommand } from "../args.js";
10
10
  import { applyRunFlags, parseRunFlags } from "../flags.js";
11
11
  import { printBrowserSessionsList } from "../usage.js";
12
+ import { isSessionOp } from "./session-ops.js";
12
13
  /** Runs a `pilot` subcommand; false when it is a session op for session-ops.ts. */
13
14
  export async function pilotCase(args) {
14
15
  if (args[1] === "sessions") {
@@ -76,26 +77,12 @@ export async function pilotCase(args) {
76
77
  }
77
78
  return true;
78
79
  }
79
- if (args[1] !== "send" &&
80
- args[1] !== "status" &&
81
- args[1] !== "dom" &&
82
- args[1] !== "trace" &&
83
- args[1] !== "console" &&
84
- args[1] !== "storage" &&
85
- args[1] !== "upload" &&
86
- args[1] !== "click" &&
87
- args[1] !== "type" &&
88
- args[1] !== "press" &&
89
- args[1] !== "goto" &&
90
- args[1] !== "reload" &&
91
- args[1] !== "reach" &&
92
- args[1] !== "resync" &&
93
- args[1] !== "highlight") {
80
+ if (!isSessionOp("pilot", args[1])) {
94
81
  console.error("waygraph pilot: usage:\n" +
95
82
  " waygraph pilot start [--inject …] [--goto <url>] [--headless] [--cli] [--base-url <url>] [project]\n" +
96
83
  " waygraph pilot sessions [project] # list live browser sessions\n" +
97
84
  " waygraph pilot attach <sessionId> # graph + snapshot for an existing session\n" +
98
- " waygraph pilot send|status|attach|highlight|… <sessionId> … (controls browser — same as browser/auto)");
85
+ " waygraph pilot send|status|read|attach|highlight|… <sessionId> … (controls browser — same as browser/auto)");
99
86
  process.exit(1);
100
87
  }
101
88
  return false;
@@ -5,6 +5,8 @@ import { parseHighlightShorthand } from "../../demo/narration/shorthand.js";
5
5
  import { resolveSessionMeta } from "../../live/auto-session/files.js";
6
6
  import { requestSession, runAttachLoop } from "../../live/auto-session/ipc.js";
7
7
  import { parseCommand } from "../args.js";
8
+ import { subscribeSession } from "../../live/auto-session/feed.js";
9
+ import { SESSION_OPS } from "../../live/auto-session/protocol.js";
8
10
  const SEND_OPTIONS = {
9
11
  timeout: { type: "string", value: "<ms>", help: "How long one Block may run (default 15000)" },
10
12
  };
@@ -13,7 +15,8 @@ const DOM_OPTIONS = {
13
15
  selector: { type: "string", value: "<css>", help: "Only this element's subtree" },
14
16
  depth: { type: "string", value: "N", help: "Levels to read" },
15
17
  };
16
- const OPS = new Set(["send", "status", "attach", "dom", "trace", "console", "storage", "upload", "click", "type", "press", "goto", "reload", "reach", "resync", "highlight"]);
18
+ /** The protocol's ops, as CLI subcommands: `subscribe` is `watch`; `attach` is the CLI's own interactive loop. */
19
+ const OPS = new Set([...SESSION_OPS.filter((op) => op !== "subscribe"), "watch", "attach"]);
17
20
  /** Whether `sub` is a session op (`attach` is pilot's own subcommand, not an op, under `pilot`). */
18
21
  export function isSessionOp(command, sub) {
19
22
  return sub !== undefined && OPS.has(sub) && !(sub === "attach" && command === "pilot");
@@ -113,6 +116,28 @@ export async function sessionOpCase(args, command) {
113
116
  process.exitCode = 1;
114
117
  return;
115
118
  }
119
+ if (sub === "hello") {
120
+ const res = await requestSession(proj, sessionId, { op: "hello" });
121
+ console.log(JSON.stringify(res));
122
+ if (!res.ok)
123
+ process.exitCode = 1;
124
+ return;
125
+ }
126
+ if (sub === "watch") {
127
+ // One JSON line per run event (steps, reads) and per new snapshot, until Ctrl-C or the session ends.
128
+ const feed = await subscribeSession(proj, sessionId, (message) => console.log(JSON.stringify(message)));
129
+ process.once("SIGINT", () => feed.close());
130
+ await feed.closed;
131
+ return;
132
+ }
133
+ if (sub === "read") {
134
+ const path = args[3];
135
+ const res = await requestSession(proj, sessionId, path ? { op: "read", path } : { op: "read" });
136
+ console.log(JSON.stringify(res));
137
+ if (!res.ok)
138
+ process.exitCode = 1;
139
+ return;
140
+ }
116
141
  if (sub === "storage") {
117
142
  const res = await requestSession(proj, sessionId, { op: "storage" });
118
143
  console.log(JSON.stringify(res));
@@ -110,7 +110,7 @@ export declare const RUN_OPTIONS: {
110
110
  };
111
111
  readonly "todo-left": {
112
112
  readonly type: "boolean";
113
- readonly help: "Floating checklist dock on the left (also ctx.todos(id, items, { pos }))";
113
+ readonly help: "Floating checklist dock on the left (also ctx.ui.todos(id, items, { pos }))";
114
114
  readonly for: readonly ["demo"];
115
115
  };
116
116
  readonly "todo-right": {
package/dist/cli/flags.js CHANGED
@@ -18,7 +18,7 @@ export const RUN_OPTIONS = {
18
18
  "fast": { type: "boolean", help: "Shorter auto-next / Next gates (keeps the smooth cursor)", for: ["demo"] },
19
19
  "full": { type: "boolean", help: "Classic wrap-all block chips (default: carousel)", for: ["demo"] },
20
20
  "mini": { type: "boolean", help: "Force the compact mini panel (Hide pill + Next)", for: ["demo"] },
21
- "todo-left": { type: "boolean", help: "Floating checklist dock on the left (also ctx.todos(id, items, { pos }))", for: ["demo"] },
21
+ "todo-left": { type: "boolean", help: "Floating checklist dock on the left (also ctx.ui.todos(id, items, { pos }))", for: ["demo"] },
22
22
  "todo-right": { type: "boolean", help: "Floating checklist dock on the right", for: ["demo"] },
23
23
  "todo-full": { type: "boolean", help: "Full checklist: no compact / collision / behind-ring", for: ["demo"] },
24
24
  "todo-smart": { type: "boolean", help: "Compact + collision + behind-ring (the default)", for: ["demo"] },
package/dist/cli/usage.js CHANGED
@@ -66,6 +66,12 @@ Primary (less is more):
66
66
  does nothing observable in the DOM) that inspectDom
67
67
  alone can't explain, since it only reads what actually
68
68
  rendered, not what the page/network actually said
69
+ waygraph auto watch <sessionId> Live feed, one JSON line each: every run event (steps,
70
+ step:read) and the snapshot after each change. Ctrl-C ends
71
+ waygraph auto hello <sessionId> The session protocol version, waygraph version, where it is
72
+ waygraph auto read <sessionId> [path] (beta) What the page shows, as JSON, from the project's
73
+ Read Blocks (*.read.block.ts); [path] narrows it, e.g.
74
+ post.comments. Read it before reaching for dom
69
75
  waygraph auto storage <sessionId> Read localStorage/sessionStorage plus registered
70
76
  service workers (scope/active URL/state) - client-
71
77
  side state (push-subscription/auth tokens etc.) that
@@ -25,136 +25,138 @@ export function createStubCtx(bag, opts) {
25
25
  mem: opts.mem,
26
26
  out: opts.out,
27
27
  error: opts.error,
28
- ring(id, stub) {
29
- bag.highlights = { ...bag.highlights, [id]: stub };
30
- },
31
- todos(id, items, opts = {}) {
32
- setDock(id, opts);
33
- bag.todos = items;
34
- if (opts.index != null)
35
- bag.todoIndex = Number(opts.index);
36
- if (!items.length)
37
- bag.hideTodos = true;
38
- },
39
- todoIndex(n) {
40
- bag.todoIndex = n;
41
- },
42
- todoGroups(id, groups, opts = {}) {
43
- setDock(id, opts);
44
- bag.todoGroups = groups;
45
- if (!groups.length)
28
+ ui: {
29
+ ring(id, stub) {
30
+ bag.highlights = { ...bag.highlights, [id]: stub };
31
+ },
32
+ todos(id, items, opts = {}) {
33
+ setDock(id, opts);
34
+ bag.todos = items;
35
+ if (opts.index != null)
36
+ bag.todoIndex = Number(opts.index);
37
+ if (!items.length)
38
+ bag.hideTodos = true;
39
+ },
40
+ todoIndex(n) {
41
+ bag.todoIndex = n;
42
+ },
43
+ todoGroups(id, groups, opts = {}) {
44
+ setDock(id, opts);
45
+ bag.todoGroups = groups;
46
+ if (!groups.length)
47
+ bag.hideTodos = true;
48
+ },
49
+ hideTodos() {
50
+ bag.todoTouched = true;
46
51
  bag.hideTodos = true;
47
- },
48
- hideTodos() {
49
- bag.todoTouched = true;
50
- bag.hideTodos = true;
51
- bag.todos = [];
52
- delete bag.todoGroups;
53
- },
54
- device(preset) {
55
- bag.deviceTouched = true;
56
- bag.hideDevice = false;
57
- const resolved = resolveDeviceState(preset, bag.touchOverride, bag.orientationOverride);
58
- if (resolved) {
59
- bag.device = resolved;
60
- if (resolved.preset === "desktop" && !resolved.touchMode) {
61
- bag.hideDevice = true;
52
+ bag.todos = [];
53
+ delete bag.todoGroups;
54
+ },
55
+ device(preset) {
56
+ bag.deviceTouched = true;
57
+ bag.hideDevice = false;
58
+ const resolved = resolveDeviceState(preset, bag.touchOverride, bag.orientationOverride);
59
+ if (resolved) {
60
+ bag.device = resolved;
61
+ if (resolved.preset === "desktop" && !resolved.touchMode) {
62
+ bag.hideDevice = true;
63
+ }
62
64
  }
63
- }
64
- },
65
- viewport(box) {
66
- bag.deviceTouched = true;
67
- bag.hideDevice = false;
68
- const resolved = resolveDeviceState(box, bag.touchOverride, bag.orientationOverride);
69
- if (resolved)
70
- bag.device = resolved;
71
- },
72
- touch(on) {
73
- bag.deviceTouched = true;
74
- const enabled = on === undefined ? true : !!on;
75
- bag.touchOverride = enabled;
76
- if (enabled)
65
+ },
66
+ viewport(box) {
67
+ bag.deviceTouched = true;
77
68
  bag.hideDevice = false;
78
- if (bag.device) {
79
- bag.device = {
80
- ...bag.device,
81
- touchMode: enabled,
82
- hasTouch: bag.device.hasTouch || enabled,
83
- };
69
+ const resolved = resolveDeviceState(box, bag.touchOverride, bag.orientationOverride);
70
+ if (resolved)
71
+ bag.device = resolved;
72
+ },
73
+ touch(on) {
74
+ bag.deviceTouched = true;
75
+ const enabled = on === undefined ? true : !!on;
76
+ bag.touchOverride = enabled;
84
77
  if (enabled)
85
78
  bag.hideDevice = false;
86
- }
87
- else if (enabled) {
88
- const base = resolveDeviceState("desktop", true, bag.orientationOverride);
79
+ if (bag.device) {
80
+ bag.device = {
81
+ ...bag.device,
82
+ touchMode: enabled,
83
+ hasTouch: bag.device.hasTouch || enabled,
84
+ };
85
+ if (enabled)
86
+ bag.hideDevice = false;
87
+ }
88
+ else if (enabled) {
89
+ const base = resolveDeviceState("desktop", true, bag.orientationOverride);
90
+ if (base)
91
+ bag.device = { ...base, touchMode: true, hasTouch: true };
92
+ }
93
+ },
94
+ orientation(o) {
95
+ const want = normalizeDeviceOrientation(o);
96
+ if (!want)
97
+ return;
98
+ bag.deviceTouched = true;
99
+ bag.hideDevice = false;
100
+ bag.orientationOverride = want;
101
+ const base = bag.device ||
102
+ resolveDeviceState("mobile", bag.touchOverride) ||
103
+ resolveDeviceState("mobile");
89
104
  if (base)
90
- bag.device = { ...base, touchMode: true, hasTouch: true };
91
- }
92
- },
93
- orientation(o) {
94
- const want = normalizeDeviceOrientation(o);
95
- if (!want)
96
- return;
97
- bag.deviceTouched = true;
98
- bag.hideDevice = false;
99
- bag.orientationOverride = want;
100
- const base = bag.device ||
101
- resolveDeviceState("mobile", bag.touchOverride) ||
102
- resolveDeviceState("mobile");
103
- if (base)
104
- bag.device = applyOrientation(base, want);
105
- },
106
- hideDevice() {
107
- bag.deviceTouched = true;
108
- bag.hideDevice = true;
109
- bag.device = resolveDeviceState("desktop", false);
110
- bag.touchOverride = false;
111
- bag.orientationOverride = undefined;
112
- },
113
- zoom(n) {
114
- if (Number.isFinite(n) && n > 0)
115
- bag.zoom = n;
116
- },
117
- zoomOut(keep) {
118
- bag.zoomOut = !!keep;
119
- },
120
- title(text, opts) {
121
- bag.title = String(text ?? "");
122
- const p = normalizeBannerPos(opts?.pos);
123
- if (p)
124
- bag.titlePos = p;
125
- },
126
- titlePos(side) {
127
- const p = normalizeBannerPos(side);
128
- if (p)
129
- bag.titlePos = p;
130
- },
131
- caption(text) {
132
- bag.caption = String(text ?? "");
133
- },
134
- slides(slides) {
135
- bag.slides = slides;
136
- },
137
- hideCaption() {
138
- bag.caption = "";
139
- },
140
- showNetworkTab() {
141
- bag.networkTabVisible = true;
142
- },
143
- hideNetworkTab() {
144
- bag.networkTabVisible = false;
145
- },
146
- bannerUi(opts) {
147
- if (!opts || typeof opts !== "object")
148
- return;
149
- bag.bannerUi = { ...(bag.bannerUi || {}), ...opts };
150
- },
151
- hideBanner() {
152
- bag.bannerUi = { ...(bag.bannerUi || {}), hidden: true };
153
- },
154
- todoDockUi(opts) {
155
- if (!opts || typeof opts !== "object")
156
- return;
157
- bag.todoDockUi = { ...(bag.todoDockUi || {}), ...opts };
105
+ bag.device = applyOrientation(base, want);
106
+ },
107
+ hideDevice() {
108
+ bag.deviceTouched = true;
109
+ bag.hideDevice = true;
110
+ bag.device = resolveDeviceState("desktop", false);
111
+ bag.touchOverride = false;
112
+ bag.orientationOverride = undefined;
113
+ },
114
+ zoom(n) {
115
+ if (Number.isFinite(n) && n > 0)
116
+ bag.zoom = n;
117
+ },
118
+ zoomOut(keep) {
119
+ bag.zoomOut = !!keep;
120
+ },
121
+ title(text, opts) {
122
+ bag.title = String(text ?? "");
123
+ const p = normalizeBannerPos(opts?.pos);
124
+ if (p)
125
+ bag.titlePos = p;
126
+ },
127
+ titlePos(side) {
128
+ const p = normalizeBannerPos(side);
129
+ if (p)
130
+ bag.titlePos = p;
131
+ },
132
+ caption(text) {
133
+ bag.caption = String(text ?? "");
134
+ },
135
+ slides(slides) {
136
+ bag.slides = slides;
137
+ },
138
+ hideCaption() {
139
+ bag.caption = "";
140
+ },
141
+ showNetworkTab() {
142
+ bag.networkTabVisible = true;
143
+ },
144
+ hideNetworkTab() {
145
+ bag.networkTabVisible = false;
146
+ },
147
+ bannerUi(opts) {
148
+ if (!opts || typeof opts !== "object")
149
+ return;
150
+ bag.bannerUi = { ...(bag.bannerUi || {}), ...opts };
151
+ },
152
+ hideBanner() {
153
+ bag.bannerUi = { ...(bag.bannerUi || {}), hidden: true };
154
+ },
155
+ todoDockUi(opts) {
156
+ if (!opts || typeof opts !== "object")
157
+ return;
158
+ bag.todoDockUi = { ...(bag.todoDockUi || {}), ...opts };
159
+ },
158
160
  },
159
161
  };
160
162
  }
@@ -51,7 +51,7 @@ export async function playSwipeTrail(page, opts) {
51
51
  layer.id = "wg-swipe-layer";
52
52
  layer.setAttribute("data-wg-ui", "1");
53
53
  layer.style.cssText =
54
- "position:fixed;inset:0;z-index:2147483646;pointer-events:none;overflow:hidden;";
54
+ "position:fixed;inset:0;z-index:var(--wg-z-cursor,2147483646);pointer-events:none;overflow:hidden;";
55
55
  const finger = document.createElement("div");
56
56
  finger.style.cssText =
57
57
  "position:absolute;width:28px;height:28px;margin:-14px 0 0 -14px;border-radius:50%;" +
@@ -2,6 +2,7 @@ export function installBanner({ title, favicon, bannerPos, bannerUi, tag: banner
2
2
  const POSITIONS = ["left", "center", "right"];
3
3
  const applyPos = (el, pos) => {
4
4
  el.dataset.pos = pos;
5
+ __wgAdd(el);
5
6
  el.dataset.home = pos;
6
7
  try {
7
8
  localStorage.setItem("wg-banner-pos", pos);
@@ -79,6 +80,7 @@ export function installBanner({ title, favicon, bannerPos, bannerUi, tag: banner
79
80
  if (bannerEl) {
80
81
  if (bui.pos && POSITIONS.includes(bui.pos) && bannerEl.dataset.home !== bui.pos) {
81
82
  bannerEl.dataset.pos = bui.pos;
83
+ __wgAdd(bannerEl);
82
84
  bannerEl.dataset.home = bui.pos;
83
85
  bannerEl.dataset.authored = "1";
84
86
  }
@@ -108,6 +110,7 @@ export function installBanner({ title, favicon, bannerPos, bannerUi, tag: banner
108
110
  (window.__wgBannerLog = window.__wgBannerLog || []).push(line);
109
111
  };
110
112
  b.dataset.pos = home;
113
+ __wgAdd(b);
111
114
  if (!hits()) {
112
115
  delete b.dataset.moved;
113
116
  log("clear");
@@ -115,6 +118,7 @@ export function installBanner({ title, favicon, bannerPos, bannerUi, tag: banner
115
118
  }
116
119
  for (const cand of POSITIONS.filter((p) => p !== home)) {
117
120
  b.dataset.pos = cand;
121
+ __wgAdd(b);
118
122
  if (!hits()) {
119
123
  b.dataset.moved = home + ">" + cand;
120
124
  log("moved:" + cand, "moved " + home + " -> " + cand + " (ring overlap)");
@@ -122,6 +126,7 @@ export function installBanner({ title, favicon, bannerPos, bannerUi, tag: banner
122
126
  }
123
127
  }
124
128
  b.dataset.pos = home;
129
+ __wgAdd(b);
125
130
  b.dataset.moved = "none-free";
126
131
  log("none-free", "overlap at every position (kept " + home + ")");
127
132
  };