browser-pilot 0.0.16 → 0.0.17

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.
@@ -3,6 +3,275 @@ import {
3
3
  stringifyUnknown
4
4
  } from "./chunk-DTVRFXKI.mjs";
5
5
 
6
+ // src/utils/strings.ts
7
+ function readString(value) {
8
+ return typeof value === "string" ? value : void 0;
9
+ }
10
+ function readStringOr(value, fallback = "") {
11
+ return readString(value) ?? fallback;
12
+ }
13
+ function formatConsoleArg(entry) {
14
+ return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
15
+ }
16
+ function globToRegex(pattern) {
17
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
18
+ const withWildcards = escaped.replace(/\*/g, ".*");
19
+ return new RegExp(`^${withWildcards}$`);
20
+ }
21
+
22
+ // src/actions/conditions.ts
23
+ var NetworkResponseTracker = class {
24
+ responses = [];
25
+ listening = false;
26
+ handler = null;
27
+ start(cdp) {
28
+ if (this.listening) return;
29
+ this.listening = true;
30
+ this.handler = (params) => {
31
+ const response = params["response"];
32
+ if (response) {
33
+ this.responses.push({ url: response.url, status: response.status });
34
+ }
35
+ };
36
+ cdp.on("Network.responseReceived", this.handler);
37
+ }
38
+ stop(cdp) {
39
+ if (this.handler) {
40
+ cdp.off("Network.responseReceived", this.handler);
41
+ this.handler = null;
42
+ }
43
+ this.listening = false;
44
+ }
45
+ getResponses() {
46
+ return this.responses;
47
+ }
48
+ reset() {
49
+ this.responses = [];
50
+ }
51
+ };
52
+ async function captureStateSignature(page) {
53
+ try {
54
+ const url = await page.url();
55
+ const text = await page.text();
56
+ const truncated = text.slice(0, 2e3);
57
+ return `${url}|${simpleHash(truncated)}`;
58
+ } catch {
59
+ return "";
60
+ }
61
+ }
62
+ function simpleHash(str) {
63
+ let hash = 0;
64
+ for (let i = 0; i < str.length; i++) {
65
+ const char = str.charCodeAt(i);
66
+ hash = (hash << 5) - hash + char | 0;
67
+ }
68
+ return hash.toString(36);
69
+ }
70
+ async function evaluateCondition(condition, page, context = {}) {
71
+ switch (condition.kind) {
72
+ case "urlMatches": {
73
+ try {
74
+ const currentUrl = await page.url();
75
+ const regex = globToRegex(condition.pattern);
76
+ const matched = regex.test(currentUrl);
77
+ return {
78
+ condition,
79
+ matched,
80
+ detail: matched ? `URL "${currentUrl}" matches "${condition.pattern}"` : `URL "${currentUrl}" does not match "${condition.pattern}"`
81
+ };
82
+ } catch {
83
+ return { condition, matched: false, detail: "Failed to get current URL" };
84
+ }
85
+ }
86
+ case "elementVisible": {
87
+ try {
88
+ const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
89
+ for (const sel of selectors) {
90
+ const visible = await page.waitFor(sel, {
91
+ timeout: 2e3,
92
+ optional: true,
93
+ state: "visible"
94
+ });
95
+ if (visible) {
96
+ return { condition, matched: true, detail: `Element "${sel}" is visible` };
97
+ }
98
+ }
99
+ return { condition, matched: false, detail: "No matching visible element found" };
100
+ } catch {
101
+ return { condition, matched: false, detail: "Visibility check failed" };
102
+ }
103
+ }
104
+ case "elementHidden": {
105
+ try {
106
+ const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
107
+ for (const sel of selectors) {
108
+ const visible = await page.waitFor(sel, {
109
+ timeout: 500,
110
+ optional: true,
111
+ state: "visible"
112
+ });
113
+ if (visible) {
114
+ return { condition, matched: false, detail: `Element "${sel}" is still visible` };
115
+ }
116
+ }
117
+ return { condition, matched: true, detail: "Element is hidden or not found" };
118
+ } catch {
119
+ return { condition, matched: true, detail: "Element is hidden (check threw)" };
120
+ }
121
+ }
122
+ case "textAppears": {
123
+ try {
124
+ const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
125
+ const text = await page.text(selector);
126
+ const matched = text.includes(condition.text);
127
+ return {
128
+ condition,
129
+ matched,
130
+ detail: matched ? `Text "${condition.text}" found` : `Text "${condition.text}" not found in page content`
131
+ };
132
+ } catch {
133
+ return { condition, matched: false, detail: "Failed to get page text" };
134
+ }
135
+ }
136
+ case "textChanges": {
137
+ try {
138
+ const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
139
+ const text = await page.text(selector);
140
+ if (condition.to !== void 0) {
141
+ const matched = text.includes(condition.to);
142
+ return {
143
+ condition,
144
+ matched,
145
+ detail: matched ? `Text changed to include "${condition.to}"` : `Text does not include "${condition.to}"`
146
+ };
147
+ }
148
+ return { condition, matched: true, detail: "textChanges without `to` defaults to true" };
149
+ } catch {
150
+ return { condition, matched: false, detail: "Failed to get text for change detection" };
151
+ }
152
+ }
153
+ case "networkResponse": {
154
+ const tracker = context.networkTracker;
155
+ if (!tracker) {
156
+ return { condition, matched: false, detail: "No network tracker active" };
157
+ }
158
+ const regex = globToRegex(condition.urlPattern);
159
+ const responses = tracker.getResponses();
160
+ for (const resp of responses) {
161
+ if (regex.test(resp.url)) {
162
+ if (condition.status !== void 0 && resp.status !== condition.status) {
163
+ continue;
164
+ }
165
+ return {
166
+ condition,
167
+ matched: true,
168
+ detail: `Network response ${resp.url} (${resp.status}) matches pattern "${condition.urlPattern}"`
169
+ };
170
+ }
171
+ }
172
+ return {
173
+ condition,
174
+ matched: false,
175
+ detail: `No network response matching "${condition.urlPattern}" (saw ${responses.length} responses)`
176
+ };
177
+ }
178
+ case "stateSignatureChanges": {
179
+ if (!context.beforeSignature) {
180
+ return { condition, matched: false, detail: "No before-signature captured" };
181
+ }
182
+ const afterSignature = await captureStateSignature(page);
183
+ const matched = afterSignature !== context.beforeSignature;
184
+ return {
185
+ condition,
186
+ matched,
187
+ detail: matched ? "Page state changed" : "Page state unchanged"
188
+ };
189
+ }
190
+ default: {
191
+ const _exhaustive = condition;
192
+ return { condition: _exhaustive, matched: false, detail: "Unknown condition kind" };
193
+ }
194
+ }
195
+ }
196
+ async function evaluateOutcome(page, options) {
197
+ const {
198
+ expectAny,
199
+ expectAll,
200
+ failIf,
201
+ dangerous = false,
202
+ networkTracker,
203
+ beforeSignature
204
+ } = options;
205
+ const allMatched = [];
206
+ const context = { networkTracker, beforeSignature };
207
+ if (failIf && failIf.length > 0) {
208
+ for (const condition of failIf) {
209
+ const result = await evaluateCondition(condition, page, context);
210
+ allMatched.push(result);
211
+ if (result.matched) {
212
+ return {
213
+ outcomeStatus: "failed",
214
+ matchedConditions: allMatched,
215
+ retrySafe: !dangerous
216
+ };
217
+ }
218
+ }
219
+ }
220
+ if (expectAll && expectAll.length > 0) {
221
+ let allPassed = true;
222
+ for (const condition of expectAll) {
223
+ const result = await evaluateCondition(condition, page, context);
224
+ allMatched.push(result);
225
+ if (!result.matched) {
226
+ allPassed = false;
227
+ }
228
+ }
229
+ if (!allPassed) {
230
+ const status = dangerous ? "unsafe_to_retry" : "ambiguous";
231
+ return {
232
+ outcomeStatus: status,
233
+ matchedConditions: allMatched,
234
+ retrySafe: !dangerous
235
+ };
236
+ }
237
+ if (!expectAny || expectAny.length === 0) {
238
+ return {
239
+ outcomeStatus: "success",
240
+ matchedConditions: allMatched,
241
+ retrySafe: true
242
+ };
243
+ }
244
+ }
245
+ if (expectAny && expectAny.length > 0) {
246
+ let anyPassed = false;
247
+ for (const condition of expectAny) {
248
+ const result = await evaluateCondition(condition, page, context);
249
+ allMatched.push(result);
250
+ if (result.matched) {
251
+ anyPassed = true;
252
+ }
253
+ }
254
+ if (anyPassed) {
255
+ return {
256
+ outcomeStatus: "success",
257
+ matchedConditions: allMatched,
258
+ retrySafe: true
259
+ };
260
+ }
261
+ const status = dangerous ? "unsafe_to_retry" : "ambiguous";
262
+ return {
263
+ outcomeStatus: status,
264
+ matchedConditions: allMatched,
265
+ retrySafe: !dangerous
266
+ };
267
+ }
268
+ return {
269
+ outcomeStatus: "success",
270
+ matchedConditions: allMatched,
271
+ retrySafe: true
272
+ };
273
+ }
274
+
6
275
  // src/actions/executor.ts
7
276
  import * as fs from "fs";
8
277
  import { join } from "path";
@@ -1469,282 +1738,6 @@ var TRACE_SCRIPT = `
1469
1738
  })();
1470
1739
  `;
1471
1740
 
1472
- // src/trace/live.ts
1473
- function globToRegex(pattern) {
1474
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1475
- const withWildcards = escaped.replace(/\*/g, ".*");
1476
- return new RegExp(`^${withWildcards}$`);
1477
- }
1478
- function readString(value) {
1479
- return typeof value === "string" ? value : void 0;
1480
- }
1481
- function readStringOr(value, fallback = "") {
1482
- return readString(value) ?? fallback;
1483
- }
1484
- function formatConsoleArg(entry) {
1485
- return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
1486
- }
1487
- var LiveTraceCollector = class {
1488
- cdp;
1489
- options;
1490
- handlers = [];
1491
- wsUrls = /* @__PURE__ */ new Map();
1492
- httpUrls = /* @__PURE__ */ new Map();
1493
- events = [];
1494
- startTime = Date.now();
1495
- matchRegex;
1496
- constructor(cdp, options = {}) {
1497
- this.cdp = cdp;
1498
- this.options = options;
1499
- this.matchRegex = options.match ? globToRegex(options.match) : null;
1500
- }
1501
- async start() {
1502
- await this.cdp.send("Runtime.enable");
1503
- await this.cdp.send("Page.enable");
1504
- await this.cdp.send("Network.enable");
1505
- await this.cdp.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
1506
- await this.cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: TRACE_SCRIPT });
1507
- await this.cdp.send("Runtime.evaluate", { expression: TRACE_SCRIPT, awaitPromise: false });
1508
- if ((this.options.mode ?? "all") !== "http") {
1509
- this.subscribe("Network.webSocketCreated", (params) => {
1510
- const requestId = readStringOr(params["requestId"]);
1511
- const url = readStringOr(params["url"]);
1512
- if (!this.matchesUrl(url)) {
1513
- return;
1514
- }
1515
- this.wsUrls.set(requestId, url);
1516
- void this.emit({
1517
- channel: "ws",
1518
- event: "ws.connection.created",
1519
- summary: `WebSocket opened ${url}`,
1520
- connectionId: requestId,
1521
- requestId,
1522
- url,
1523
- data: { url }
1524
- });
1525
- });
1526
- this.subscribe("Network.webSocketFrameSent", (params) => {
1527
- const requestId = readStringOr(params["requestId"]);
1528
- const response = params["response"];
1529
- const payload = this.formatPayload(response?.payloadData, response?.opcode ?? 1);
1530
- const url = this.wsUrls.get(requestId);
1531
- if (this.matchRegex && !this.matchRegex.test(url ?? "") && !this.matchRegex.test(payload)) {
1532
- return;
1533
- }
1534
- void this.emit({
1535
- channel: "ws",
1536
- event: "ws.frame.sent",
1537
- summary: `WebSocket frame sent ${requestId}`,
1538
- connectionId: requestId,
1539
- requestId,
1540
- url,
1541
- data: {
1542
- opcode: response?.opcode ?? 1,
1543
- payload,
1544
- length: response?.payloadData?.length ?? 0
1545
- }
1546
- });
1547
- });
1548
- this.subscribe("Network.webSocketFrameReceived", (params) => {
1549
- const requestId = readStringOr(params["requestId"]);
1550
- const response = params["response"];
1551
- const payload = this.formatPayload(response?.payloadData, response?.opcode ?? 1);
1552
- const url = this.wsUrls.get(requestId);
1553
- if (this.matchRegex && !this.matchRegex.test(url ?? "") && !this.matchRegex.test(payload)) {
1554
- return;
1555
- }
1556
- void this.emit({
1557
- channel: "ws",
1558
- event: "ws.frame.received",
1559
- summary: `WebSocket frame received ${requestId}`,
1560
- connectionId: requestId,
1561
- requestId,
1562
- url,
1563
- data: {
1564
- opcode: response?.opcode ?? 1,
1565
- payload,
1566
- length: response?.payloadData?.length ?? 0
1567
- }
1568
- });
1569
- });
1570
- this.subscribe("Network.webSocketClosed", (params) => {
1571
- const requestId = readStringOr(params["requestId"]);
1572
- const url = this.wsUrls.get(requestId);
1573
- this.wsUrls.delete(requestId);
1574
- void this.emit({
1575
- channel: "ws",
1576
- event: "ws.connection.closed",
1577
- summary: `WebSocket closed ${requestId}`,
1578
- severity: "warn",
1579
- connectionId: requestId,
1580
- requestId,
1581
- url,
1582
- data: { url }
1583
- });
1584
- });
1585
- }
1586
- if ((this.options.mode ?? "all") !== "ws") {
1587
- this.subscribe("Network.requestWillBeSent", (params) => {
1588
- const request = params["request"];
1589
- const requestId = readStringOr(params["requestId"]);
1590
- const url = request?.url ?? "";
1591
- if (!this.matchesUrl(url)) {
1592
- return;
1593
- }
1594
- this.httpUrls.set(requestId, url);
1595
- void this.emit({
1596
- channel: "http",
1597
- event: "http.request.sent",
1598
- summary: `${request?.method ?? "GET"} ${url}`,
1599
- requestId,
1600
- url,
1601
- data: {
1602
- method: request?.method ?? "GET",
1603
- headers: request?.headers ?? {},
1604
- body: request?.postData ?? null
1605
- }
1606
- });
1607
- });
1608
- this.subscribe("Network.responseReceived", (params) => {
1609
- const requestId = readStringOr(params["requestId"]);
1610
- if (!this.httpUrls.has(requestId)) {
1611
- return;
1612
- }
1613
- const response = params["response"];
1614
- void this.emit({
1615
- channel: "http",
1616
- event: "http.response.received",
1617
- summary: `${response?.status ?? 0} ${response?.url ?? this.httpUrls.get(requestId) ?? ""}`,
1618
- requestId,
1619
- url: response?.url ?? this.httpUrls.get(requestId),
1620
- data: {
1621
- status: response?.status ?? 0,
1622
- headers: response?.headers ?? {},
1623
- mimeType: response?.mimeType ?? null
1624
- }
1625
- });
1626
- });
1627
- this.subscribe("Network.loadingFailed", (params) => {
1628
- const requestId = readStringOr(params["requestId"]);
1629
- const url = readString(params["blockedReason"]) ?? this.httpUrls.get(requestId) ?? "";
1630
- void this.emit({
1631
- channel: "http",
1632
- event: "http.response.failed",
1633
- summary: `HTTP request failed ${requestId}`,
1634
- severity: "error",
1635
- requestId,
1636
- url,
1637
- data: {
1638
- errorText: params["errorText"] ?? null,
1639
- blockedReason: params["blockedReason"] ?? null,
1640
- canceled: params["canceled"] ?? false
1641
- }
1642
- });
1643
- });
1644
- }
1645
- this.subscribe("Runtime.consoleAPICalled", (params) => {
1646
- const type = readStringOr(params["type"], "log");
1647
- if (type !== "log" && type !== "warn" && type !== "error") {
1648
- return;
1649
- }
1650
- const args = Array.isArray(params["args"]) ? params["args"] : [];
1651
- const text = args.map(formatConsoleArg).filter(Boolean).join(" ");
1652
- void this.emit({
1653
- channel: "console",
1654
- event: `console.${type}`,
1655
- severity: type === "error" ? "error" : type === "warn" ? "warn" : "info",
1656
- summary: text || `console.${type}`,
1657
- data: { args }
1658
- });
1659
- });
1660
- this.subscribe("Runtime.exceptionThrown", (params) => {
1661
- const details = params["exceptionDetails"] ?? {};
1662
- const text = readString(details["text"]) ?? "Runtime exception";
1663
- void this.emit({
1664
- channel: "runtime",
1665
- event: "runtime.exception",
1666
- severity: "error",
1667
- summary: text,
1668
- data: details
1669
- });
1670
- });
1671
- this.subscribe("Runtime.bindingCalled", (params) => {
1672
- if (params["name"] !== TRACE_BINDING_NAME) {
1673
- return;
1674
- }
1675
- const raw = readStringOr(params["payload"]);
1676
- try {
1677
- const payload = JSON.parse(raw);
1678
- const channel = this.channelForTraceEvent(payload.event);
1679
- void this.emit({
1680
- channel,
1681
- event: payload.event,
1682
- severity: payload.severity,
1683
- summary: payload.summary ?? payload.event,
1684
- ts: payload.ts ? new Date(payload.ts).toISOString() : void 0,
1685
- data: payload.data ?? {},
1686
- url: readString(payload.data?.["url"])
1687
- });
1688
- } catch {
1689
- }
1690
- });
1691
- }
1692
- async stop() {
1693
- for (const { event, handler } of this.handlers) {
1694
- this.cdp.off(event, handler);
1695
- }
1696
- this.handlers.length = 0;
1697
- return [...this.events];
1698
- }
1699
- getEvents() {
1700
- return [...this.events];
1701
- }
1702
- subscribe(event, handler) {
1703
- this.cdp.on(event, handler);
1704
- this.handlers.push({ event, handler });
1705
- }
1706
- matchesUrl(url) {
1707
- if (!this.matchRegex) {
1708
- return true;
1709
- }
1710
- return this.matchRegex.test(url);
1711
- }
1712
- formatPayload(payloadData, opcode) {
1713
- const data = payloadData ?? "";
1714
- const maxPayload = this.options.maxPayload ?? 256;
1715
- if (opcode === 2) {
1716
- const byteLength = Math.floor(data.length * 3 / 4);
1717
- return `[binary: ${byteLength} bytes]`;
1718
- }
1719
- if (data.length > maxPayload) {
1720
- return `${data.slice(0, maxPayload)}... [truncated, ${data.length} total]`;
1721
- }
1722
- return data;
1723
- }
1724
- channelForTraceEvent(eventName) {
1725
- if (eventName.startsWith("ws.")) return "ws";
1726
- if (eventName.startsWith("http.")) return "http";
1727
- if (eventName.startsWith("console.")) return "console";
1728
- if (eventName.startsWith("permission.")) return "permission";
1729
- if (eventName.startsWith("media.")) return "media";
1730
- if (eventName.startsWith("voice.")) return "voice";
1731
- if (eventName.startsWith("dom.")) return "dom";
1732
- if (eventName.startsWith("runtime.")) return "runtime";
1733
- return "session";
1734
- }
1735
- async emit(event) {
1736
- const normalized = normalizeTraceEvent({
1737
- traceId: event.traceId ?? createTraceId(event.channel),
1738
- sessionId: this.options.sessionId,
1739
- targetId: this.options.targetId,
1740
- elapsedMs: event.elapsedMs ?? Date.now() - this.startTime,
1741
- ...event
1742
- });
1743
- this.events.push(normalized);
1744
- await this.options.onEvent?.(normalized);
1745
- }
1746
- };
1747
-
1748
1741
  // src/actions/executor.ts
1749
1742
  var DEFAULT_TIMEOUT = 3e4;
1750
1743
  var DEFAULT_RECORDING_SKIP_ACTIONS = [
@@ -1754,15 +1747,6 @@ var DEFAULT_RECORDING_SKIP_ACTIONS = [
1754
1747
  "text",
1755
1748
  "screenshot"
1756
1749
  ];
1757
- function readString2(value) {
1758
- return typeof value === "string" ? value : void 0;
1759
- }
1760
- function readStringOr2(value, fallback = "") {
1761
- return readString2(value) ?? fallback;
1762
- }
1763
- function formatConsoleArg2(entry) {
1764
- return readString2(entry["value"]) ?? readString2(entry["description"]) ?? "";
1765
- }
1766
1750
  function loadExistingRecording(manifestPath) {
1767
1751
  try {
1768
1752
  const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
@@ -1876,6 +1860,25 @@ function getSuggestion(reason) {
1876
1860
  }
1877
1861
  }
1878
1862
  }
1863
+ function hasOutcomeConditions(step) {
1864
+ return step.expectAny !== void 0 && step.expectAny.length > 0 || step.expectAll !== void 0 && step.expectAll.length > 0 || step.failIf !== void 0 && step.failIf.length > 0;
1865
+ }
1866
+ function needsNetworkTracking(step) {
1867
+ const allConditions = [
1868
+ ...step.expectAny ?? [],
1869
+ ...step.expectAll ?? [],
1870
+ ...step.failIf ?? []
1871
+ ];
1872
+ return allConditions.some((c) => c.kind === "networkResponse");
1873
+ }
1874
+ function needsStateSignature(step) {
1875
+ const allConditions = [
1876
+ ...step.expectAny ?? [],
1877
+ ...step.expectAll ?? [],
1878
+ ...step.failIf ?? []
1879
+ ];
1880
+ return allConditions.some((c) => c.kind === "stateSignatureChanges");
1881
+ }
1879
1882
  var BatchExecutor = class {
1880
1883
  page;
1881
1884
  constructor(page) {
@@ -1921,9 +1924,25 @@ var BatchExecutor = class {
1921
1924
  })
1922
1925
  );
1923
1926
  }
1927
+ const hasOutcome = hasOutcomeConditions(step);
1928
+ let networkTracker;
1929
+ let beforeSignature;
1930
+ if (hasOutcome) {
1931
+ if (needsNetworkTracking(step)) {
1932
+ networkTracker = new NetworkResponseTracker();
1933
+ networkTracker.start(this.page.cdpClient);
1934
+ }
1935
+ if (needsStateSignature(step)) {
1936
+ beforeSignature = await captureStateSignature(this.page);
1937
+ }
1938
+ }
1924
1939
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
1925
1940
  if (attempt > 0) {
1926
1941
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
1942
+ if (networkTracker) networkTracker.reset();
1943
+ if (hasOutcome && needsStateSignature(step)) {
1944
+ beforeSignature = await captureStateSignature(this.page);
1945
+ }
1927
1946
  }
1928
1947
  try {
1929
1948
  this.page.resetLastActionPosition();
@@ -1941,6 +1960,28 @@ var BatchExecutor = class {
1941
1960
  coordinates: this.page.getLastActionCoordinates() ?? void 0,
1942
1961
  boundingBox: this.page.getLastActionBoundingBox() ?? void 0
1943
1962
  };
1963
+ if (hasOutcome) {
1964
+ if (networkTracker) networkTracker.stop(this.page.cdpClient);
1965
+ const outcome = await evaluateOutcome(this.page, {
1966
+ expectAny: step.expectAny,
1967
+ expectAll: step.expectAll,
1968
+ failIf: step.failIf,
1969
+ dangerous: step.dangerous,
1970
+ networkTracker,
1971
+ beforeSignature
1972
+ });
1973
+ stepResult.outcomeStatus = outcome.outcomeStatus;
1974
+ stepResult.matchedConditions = outcome.matchedConditions;
1975
+ stepResult.retrySafe = outcome.retrySafe;
1976
+ if (outcome.outcomeStatus !== "success") {
1977
+ stepResult.success = false;
1978
+ stepResult.error = `Outcome: ${outcome.outcomeStatus}`;
1979
+ const failedDetails = outcome.matchedConditions.filter((mc) => outcome.outcomeStatus === "failed" ? mc.matched : !mc.matched).map((mc) => mc.detail).filter(Boolean);
1980
+ if (failedDetails.length > 0) {
1981
+ stepResult.suggestion = failedDetails.join("; ");
1982
+ }
1983
+ }
1984
+ }
1944
1985
  if (recording && !recording.skipActions.has(step.action)) {
1945
1986
  await this.captureRecordingFrame(step, stepResult, recording);
1946
1987
  }
@@ -1950,13 +1991,14 @@ var BatchExecutor = class {
1950
1991
  traceId: createTraceId("action"),
1951
1992
  elapsedMs: Date.now() - startTime,
1952
1993
  channel: "action",
1953
- event: "action.succeeded",
1954
- summary: `${step.action} succeeded`,
1994
+ event: stepResult.success ? "action.succeeded" : "action.outcome_failed",
1995
+ summary: stepResult.success ? `${step.action} succeeded` : `${step.action} outcome: ${stepResult.outcomeStatus}`,
1955
1996
  data: {
1956
1997
  action: step.action,
1957
1998
  selector: step.selector ?? null,
1958
1999
  selectorUsed: result.selectorUsed ?? null,
1959
- durationMs: Date.now() - stepStart
2000
+ durationMs: Date.now() - stepStart,
2001
+ outcomeStatus: stepResult.outcomeStatus ?? null
1960
2002
  },
1961
2003
  actionId: `action-${i + 1}`,
1962
2004
  stepIndex: i,
@@ -1966,6 +2008,18 @@ var BatchExecutor = class {
1966
2008
  })
1967
2009
  );
1968
2010
  }
2011
+ if (hasOutcome && !stepResult.success) {
2012
+ if (step.dangerous) {
2013
+ results.push(stepResult);
2014
+ break;
2015
+ }
2016
+ if (attempt < maxAttempts - 1) {
2017
+ lastError = new Error(stepResult.error ?? "Outcome failed");
2018
+ continue;
2019
+ }
2020
+ results.push(stepResult);
2021
+ break;
2022
+ }
1969
2023
  results.push(stepResult);
1970
2024
  succeeded = true;
1971
2025
  break;
@@ -1973,59 +2027,63 @@ var BatchExecutor = class {
1973
2027
  lastError = error instanceof Error ? error : new Error(String(error));
1974
2028
  }
1975
2029
  }
2030
+ if (networkTracker) networkTracker.stop(this.page.cdpClient);
1976
2031
  if (!succeeded) {
1977
- const errorMessage = lastError?.message ?? "Unknown error";
1978
- let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
1979
- const { reason, coveringElement } = classifyFailure(lastError);
1980
- if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
1981
- try {
1982
- const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
1983
- const autoHints = await generateHints(this.page, selectors, step.action, 3);
1984
- if (autoHints.length > 0) {
1985
- hints = autoHints;
2032
+ const resultAlreadyPushed = results.length > 0 && results[results.length - 1].index === i;
2033
+ if (!resultAlreadyPushed) {
2034
+ const errorMessage = lastError?.message ?? "Unknown error";
2035
+ let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
2036
+ const { reason, coveringElement } = classifyFailure(lastError);
2037
+ if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
2038
+ try {
2039
+ const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
2040
+ const autoHints = await generateHints(this.page, selectors, step.action, 3);
2041
+ if (autoHints.length > 0) {
2042
+ hints = autoHints;
2043
+ }
2044
+ } catch {
1986
2045
  }
1987
- } catch {
1988
2046
  }
2047
+ const failedResult = {
2048
+ index: i,
2049
+ action: step.action,
2050
+ selector: step.selector,
2051
+ success: false,
2052
+ durationMs: Date.now() - stepStart,
2053
+ error: errorMessage,
2054
+ hints,
2055
+ failureReason: reason,
2056
+ coveringElement,
2057
+ suggestion: getSuggestion(reason),
2058
+ timestamp: Date.now()
2059
+ };
2060
+ if (recording && !recording.skipActions.has(step.action)) {
2061
+ await this.captureRecordingFrame(step, failedResult, recording);
2062
+ }
2063
+ if (recording) {
2064
+ recording.traceEvents.push(
2065
+ normalizeTraceEvent({
2066
+ traceId: createTraceId("action"),
2067
+ elapsedMs: Date.now() - startTime,
2068
+ channel: "action",
2069
+ event: "action.failed",
2070
+ severity: "error",
2071
+ summary: `${step.action} failed: ${errorMessage}`,
2072
+ data: {
2073
+ action: step.action,
2074
+ selector: step.selector ?? null,
2075
+ error: errorMessage,
2076
+ reason
2077
+ },
2078
+ actionId: `action-${i + 1}`,
2079
+ stepIndex: i,
2080
+ selector: step.selector,
2081
+ url: step.url
2082
+ })
2083
+ );
2084
+ }
2085
+ results.push(failedResult);
1989
2086
  }
1990
- const failedResult = {
1991
- index: i,
1992
- action: step.action,
1993
- selector: step.selector,
1994
- success: false,
1995
- durationMs: Date.now() - stepStart,
1996
- error: errorMessage,
1997
- hints,
1998
- failureReason: reason,
1999
- coveringElement,
2000
- suggestion: getSuggestion(reason),
2001
- timestamp: Date.now()
2002
- };
2003
- if (recording && !recording.skipActions.has(step.action)) {
2004
- await this.captureRecordingFrame(step, failedResult, recording);
2005
- }
2006
- if (recording) {
2007
- recording.traceEvents.push(
2008
- normalizeTraceEvent({
2009
- traceId: createTraceId("action"),
2010
- elapsedMs: Date.now() - startTime,
2011
- channel: "action",
2012
- event: "action.failed",
2013
- severity: "error",
2014
- summary: `${step.action} failed: ${errorMessage}`,
2015
- data: {
2016
- action: step.action,
2017
- selector: step.selector ?? null,
2018
- error: errorMessage,
2019
- reason
2020
- },
2021
- actionId: `action-${i + 1}`,
2022
- stepIndex: i,
2023
- selector: step.selector,
2024
- url: step.url
2025
- })
2026
- );
2027
- }
2028
- results.push(failedResult);
2029
2087
  if (onFail === "stop" && !step.optional) {
2030
2088
  stoppedAtIndex = i;
2031
2089
  break;
@@ -2336,6 +2394,14 @@ var BatchExecutor = class {
2336
2394
  case "forms": {
2337
2395
  return { value: await this.page.forms() };
2338
2396
  }
2397
+ case "delta": {
2398
+ const review = await this.page.review();
2399
+ return { value: review };
2400
+ }
2401
+ case "review": {
2402
+ const review = await this.page.review();
2403
+ return { value: review };
2404
+ }
2339
2405
  case "screenshot": {
2340
2406
  const data = await this.page.screenshot({
2341
2407
  format: step.format,
@@ -2486,6 +2552,35 @@ var BatchExecutor = class {
2486
2552
  const media = await this.assertMediaTrackLive(step.kind);
2487
2553
  return { value: media };
2488
2554
  }
2555
+ case "chooseOption": {
2556
+ const { chooseOption } = await import("./combobox-RAKBA2BW.mjs");
2557
+ if (!step.value) throw new Error("chooseOption requires value");
2558
+ const result = await chooseOption(this.page, {
2559
+ trigger: step.trigger ?? step.selector ?? "",
2560
+ listbox: step.option ? Array.isArray(step.option) ? step.option : [step.option] : void 0,
2561
+ value: typeof step.value === "string" ? step.value : step.value[0] ?? "",
2562
+ match: step.match,
2563
+ timeout: step.timeout ?? timeout
2564
+ });
2565
+ if (!result.success) {
2566
+ throw new Error(result.error ?? `chooseOption failed at ${result.failedAt}`);
2567
+ }
2568
+ return { value: result };
2569
+ }
2570
+ case "upload": {
2571
+ const { uploadFiles } = await import("./upload-E6MCC2OF.mjs");
2572
+ if (!step.selector) throw new Error("upload requires selector");
2573
+ if (!step.files || step.files.length === 0) throw new Error("upload requires files");
2574
+ const result = await uploadFiles(this.page, {
2575
+ selector: step.selector,
2576
+ files: step.files,
2577
+ timeout: step.timeout ?? timeout
2578
+ });
2579
+ if (!result.accepted) {
2580
+ throw new Error(result.error ?? "Upload was not accepted");
2581
+ }
2582
+ return { value: result };
2583
+ }
2489
2584
  default: {
2490
2585
  const action = step.action;
2491
2586
  const aliases = {
@@ -2587,10 +2682,10 @@ Valid actions: ${valid}`);
2587
2682
  clearTimeout(timer);
2588
2683
  };
2589
2684
  const onCreated = (params) => {
2590
- wsUrls.set(readStringOr2(params["requestId"]), readStringOr2(params["url"]));
2685
+ wsUrls.set(readStringOr(params["requestId"]), readStringOr(params["url"]));
2591
2686
  };
2592
2687
  const onFrame = (params) => {
2593
- const requestId = readStringOr2(params["requestId"]);
2688
+ const requestId = readStringOr(params["requestId"]);
2594
2689
  const response = params["response"] ?? {};
2595
2690
  const payload = response.payloadData ?? "";
2596
2691
  const url = wsUrls.get(requestId) ?? "";
@@ -2608,13 +2703,13 @@ Valid actions: ${valid}`);
2608
2703
  return;
2609
2704
  }
2610
2705
  try {
2611
- const parsed = JSON.parse(readStringOr2(params["payload"]));
2706
+ const parsed = JSON.parse(readStringOr(params["payload"]));
2612
2707
  if (parsed.event !== "ws.frame.received") {
2613
2708
  return;
2614
2709
  }
2615
2710
  const data = parsed.data ?? {};
2616
- const payload = readStringOr2(data["payload"]);
2617
- const url = readStringOr2(data["url"]);
2711
+ const payload = readStringOr(data["payload"]);
2712
+ const url = readStringOr(data["url"]);
2618
2713
  if (!regex.test(url) && !regex.test(payload)) {
2619
2714
  return;
2620
2715
  }
@@ -2623,7 +2718,7 @@ Valid actions: ${valid}`);
2623
2718
  }
2624
2719
  cleanup();
2625
2720
  resolve({
2626
- requestId: readStringOr2(data["connectionId"]),
2721
+ requestId: readStringOr(data["connectionId"]),
2627
2722
  url,
2628
2723
  payload
2629
2724
  });
@@ -2668,13 +2763,13 @@ Valid actions: ${valid}`);
2668
2763
  continue;
2669
2764
  }
2670
2765
  const record = entry;
2671
- const event = readStringOr2(record["event"]);
2766
+ const event = readStringOr(record["event"]);
2672
2767
  if (event !== "ws.frame.received") {
2673
2768
  continue;
2674
2769
  }
2675
2770
  const data = record["data"] ?? {};
2676
- const payload = readStringOr2(data["payload"]);
2677
- const url = readStringOr2(data["url"]);
2771
+ const payload = readStringOr(data["payload"]);
2772
+ const url = readStringOr(data["url"]);
2678
2773
  if (!regex.test(url) && !regex.test(payload)) {
2679
2774
  continue;
2680
2775
  }
@@ -2682,7 +2777,7 @@ Valid actions: ${valid}`);
2682
2777
  continue;
2683
2778
  }
2684
2779
  return {
2685
- requestId: readStringOr2(data["connectionId"]),
2780
+ requestId: readStringOr(data["connectionId"]),
2686
2781
  url,
2687
2782
  payload
2688
2783
  };
@@ -2703,11 +2798,11 @@ Valid actions: ${valid}`);
2703
2798
  return;
2704
2799
  }
2705
2800
  const args = Array.isArray(params["args"]) ? params["args"] : [];
2706
- errors.push(args.map(formatConsoleArg2).filter(Boolean).join(" "));
2801
+ errors.push(args.map(formatConsoleArg).filter(Boolean).join(" "));
2707
2802
  };
2708
2803
  const onException = (params) => {
2709
2804
  const details = params["exceptionDetails"] ?? {};
2710
- errors.push(readString2(details["text"]) ?? "Runtime exception");
2805
+ errors.push(readString(details["text"]) ?? "Runtime exception");
2711
2806
  };
2712
2807
  const timer = setTimeout(() => {
2713
2808
  cleanup();
@@ -3079,6 +3174,30 @@ var ACTION_RULES = {
3079
3174
  kind: { type: "string", enum: ["audio", "video"] }
3080
3175
  },
3081
3176
  optional: {}
3177
+ },
3178
+ delta: {
3179
+ required: {},
3180
+ optional: {}
3181
+ },
3182
+ review: {
3183
+ required: {},
3184
+ optional: {}
3185
+ },
3186
+ chooseOption: {
3187
+ required: { value: { type: "string|string[]" } },
3188
+ optional: {
3189
+ trigger: { type: "string|string[]" },
3190
+ selector: { type: "string|string[]" },
3191
+ option: { type: "string|string[]" },
3192
+ match: { type: "string", enum: ["exact", "contains", "startsWith"] }
3193
+ }
3194
+ },
3195
+ upload: {
3196
+ required: {
3197
+ selector: { type: "string|string[]" },
3198
+ files: { type: "string|string[]" }
3199
+ },
3200
+ optional: {}
3082
3201
  }
3083
3202
  };
3084
3203
  var VALID_ACTIONS = Object.keys(ACTION_RULES);
@@ -3118,7 +3237,12 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
3118
3237
  "name",
3119
3238
  "state",
3120
3239
  "kind",
3121
- "windowMs"
3240
+ "windowMs",
3241
+ "expectAny",
3242
+ "expectAll",
3243
+ "failIf",
3244
+ "dangerous",
3245
+ "files"
3122
3246
  ]);
3123
3247
  function resolveAction(name) {
3124
3248
  if (VALID_ACTIONS.includes(name)) {
@@ -3338,6 +3462,64 @@ function validateSteps(steps) {
3338
3462
  });
3339
3463
  }
3340
3464
  }
3465
+ if ("dangerous" in obj && obj["dangerous"] !== void 0) {
3466
+ if (typeof obj["dangerous"] !== "boolean") {
3467
+ errors.push({
3468
+ stepIndex: i,
3469
+ field: "dangerous",
3470
+ message: `"dangerous" expected boolean, got ${typeof obj["dangerous"]}.`
3471
+ });
3472
+ }
3473
+ }
3474
+ for (const condField of ["expectAny", "expectAll", "failIf"]) {
3475
+ if (condField in obj && obj[condField] !== void 0) {
3476
+ if (!Array.isArray(obj[condField])) {
3477
+ errors.push({
3478
+ stepIndex: i,
3479
+ field: condField,
3480
+ message: `"${condField}" expected array, got ${typeof obj[condField]}.`
3481
+ });
3482
+ } else {
3483
+ const conditions = obj[condField];
3484
+ for (let ci = 0; ci < conditions.length; ci++) {
3485
+ const cond = conditions[ci];
3486
+ if (!cond || typeof cond !== "object" || Array.isArray(cond)) {
3487
+ errors.push({
3488
+ stepIndex: i,
3489
+ field: condField,
3490
+ message: `"${condField}[${ci}]" must be a condition object.`
3491
+ });
3492
+ continue;
3493
+ }
3494
+ const condObj = cond;
3495
+ if (!("kind" in condObj) || typeof condObj["kind"] !== "string") {
3496
+ errors.push({
3497
+ stepIndex: i,
3498
+ field: condField,
3499
+ message: `"${condField}[${ci}]" missing required "kind" field.`
3500
+ });
3501
+ } else {
3502
+ const validKinds = [
3503
+ "urlMatches",
3504
+ "elementVisible",
3505
+ "elementHidden",
3506
+ "textAppears",
3507
+ "textChanges",
3508
+ "networkResponse",
3509
+ "stateSignatureChanges"
3510
+ ];
3511
+ if (!validKinds.includes(condObj["kind"])) {
3512
+ errors.push({
3513
+ stepIndex: i,
3514
+ field: condField,
3515
+ message: `"${condField}[${ci}].kind" must be one of: ${validKinds.join(", ")}. Got "${condObj["kind"]}".`
3516
+ });
3517
+ }
3518
+ }
3519
+ }
3520
+ }
3521
+ }
3522
+ }
3341
3523
  if (action === "assertText") {
3342
3524
  if (!("expect" in obj) && !("value" in obj)) {
3343
3525
  errors.push({
@@ -5398,6 +5580,114 @@ async function waitForNetworkIdle(cdp, options = {}) {
5398
5580
  });
5399
5581
  }
5400
5582
 
5583
+ // src/browser/delta.ts
5584
+ function extractPageState(url, title, snapshot, forms, pageText) {
5585
+ const headings = [];
5586
+ const buttons = [];
5587
+ const alerts = [];
5588
+ function walkNodes(nodes) {
5589
+ for (const node of nodes) {
5590
+ const role = node.role?.toLowerCase() ?? "";
5591
+ if (role === "heading" && node.name) {
5592
+ headings.push(node.name);
5593
+ }
5594
+ if ((role === "button" || role === "link") && node.name) {
5595
+ const disabled = node.disabled ?? false;
5596
+ buttons.push({ text: node.name, disabled, ref: node.ref });
5597
+ }
5598
+ if (role === "alert" && node.name) {
5599
+ alerts.push(node.name);
5600
+ }
5601
+ if (node.children) {
5602
+ walkNodes(node.children);
5603
+ }
5604
+ }
5605
+ }
5606
+ walkNodes(snapshot.accessibilityTree);
5607
+ const formFields = forms.map((f) => ({
5608
+ label: f.label,
5609
+ name: f.name,
5610
+ id: f.id,
5611
+ value: f.value,
5612
+ type: f.type
5613
+ }));
5614
+ return {
5615
+ url,
5616
+ title,
5617
+ headings,
5618
+ formFields,
5619
+ buttons,
5620
+ alerts,
5621
+ visibleText: pageText.slice(0, 3e3)
5622
+ };
5623
+ }
5624
+ function computeDelta(before, after) {
5625
+ const changes = [];
5626
+ if (before.url !== after.url) {
5627
+ changes.push({ kind: "url", before: before.url, after: after.url });
5628
+ }
5629
+ if (before.title !== after.title) {
5630
+ changes.push({ kind: "title", before: before.title, after: after.title });
5631
+ }
5632
+ const beforeHeadings = new Set(before.headings);
5633
+ const afterHeadings = new Set(after.headings);
5634
+ for (const h of after.headings) {
5635
+ if (!beforeHeadings.has(h)) {
5636
+ changes.push({ kind: "heading_added", after: h });
5637
+ }
5638
+ }
5639
+ for (const h of before.headings) {
5640
+ if (!afterHeadings.has(h)) {
5641
+ changes.push({ kind: "heading_removed", before: h });
5642
+ }
5643
+ }
5644
+ const beforeFieldMap = new Map(
5645
+ before.formFields.map((f) => [f.id ?? f.name ?? f.label ?? "", f])
5646
+ );
5647
+ for (const af of after.formFields) {
5648
+ const key = af.id ?? af.name ?? af.label ?? "";
5649
+ const bf = beforeFieldMap.get(key);
5650
+ if (bf && JSON.stringify(bf.value) !== JSON.stringify(af.value)) {
5651
+ changes.push({
5652
+ kind: "field_changed",
5653
+ before: String(bf.value ?? ""),
5654
+ after: String(af.value ?? ""),
5655
+ detail: af.label ?? af.name ?? af.id ?? key
5656
+ });
5657
+ }
5658
+ }
5659
+ const beforeBtnMap = new Map(before.buttons.map((b) => [b.text, b]));
5660
+ for (const ab of after.buttons) {
5661
+ const bb = beforeBtnMap.get(ab.text);
5662
+ if (bb && bb.disabled !== ab.disabled) {
5663
+ changes.push({
5664
+ kind: "button_changed",
5665
+ detail: ab.text,
5666
+ before: bb.disabled ? "disabled" : "enabled",
5667
+ after: ab.disabled ? "disabled" : "enabled"
5668
+ });
5669
+ }
5670
+ }
5671
+ const beforeAlerts = new Set(before.alerts);
5672
+ const afterAlerts = new Set(after.alerts);
5673
+ for (const a of after.alerts) {
5674
+ if (!beforeAlerts.has(a)) {
5675
+ changes.push({ kind: "alert_added", after: a });
5676
+ }
5677
+ }
5678
+ for (const a of before.alerts) {
5679
+ if (!afterAlerts.has(a)) {
5680
+ changes.push({ kind: "alert_removed", before: a });
5681
+ }
5682
+ }
5683
+ return {
5684
+ changes,
5685
+ before,
5686
+ after,
5687
+ hasChanges: changes.length > 0
5688
+ };
5689
+ }
5690
+
5401
5691
  // src/browser/keyboard.ts
5402
5692
  var US_KEYBOARD = {
5403
5693
  // Letters (lowercase)
@@ -5557,8 +5847,118 @@ function parseShortcut(combo) {
5557
5847
  return { modifiers, key };
5558
5848
  }
5559
5849
 
5850
+ // src/browser/review.ts
5851
+ function extractReview(url, title, snapshot, forms, pageText) {
5852
+ const headings = [];
5853
+ const alerts = [];
5854
+ const statusLabels = [];
5855
+ const keyValues = [];
5856
+ const tables = [];
5857
+ const summaryCards = [];
5858
+ function walkNodes(nodes, parentHeading) {
5859
+ let currentHeading = parentHeading;
5860
+ for (const node of nodes) {
5861
+ const role = node.role?.toLowerCase() ?? "";
5862
+ if (role === "heading" && node.name) {
5863
+ headings.push(node.name);
5864
+ currentHeading = node.name;
5865
+ }
5866
+ if (role === "alert" && node.name) {
5867
+ alerts.push(node.name);
5868
+ }
5869
+ if (role === "status" && node.name) {
5870
+ statusLabels.push(node.name);
5871
+ }
5872
+ if (role === "table" || role === "grid") {
5873
+ const table = extractTableFromNode(node);
5874
+ if (table) tables.push(table);
5875
+ }
5876
+ if ((role === "definition" || role === "term") && node.name) {
5877
+ if (role === "term") {
5878
+ keyValues.push({ key: node.name, value: "" });
5879
+ } else if (role === "definition" && keyValues.length > 0) {
5880
+ const last = keyValues[keyValues.length - 1];
5881
+ if (!last.value) last.value = node.name;
5882
+ }
5883
+ }
5884
+ if (node.children) {
5885
+ walkNodes(node.children, currentHeading);
5886
+ }
5887
+ }
5888
+ }
5889
+ walkNodes(snapshot.accessibilityTree);
5890
+ const textKvPairs = extractKeyValueFromText(pageText);
5891
+ keyValues.push(...textKvPairs);
5892
+ const formEntries = forms.map((f) => ({
5893
+ label: f.label,
5894
+ value: f.value,
5895
+ type: f.type,
5896
+ disabled: f.disabled
5897
+ }));
5898
+ return {
5899
+ url,
5900
+ title,
5901
+ headings,
5902
+ forms: formEntries,
5903
+ alerts,
5904
+ summaryCards,
5905
+ tables,
5906
+ keyValues,
5907
+ statusLabels
5908
+ };
5909
+ }
5910
+ function extractTableFromNode(node) {
5911
+ const headers = [];
5912
+ const rows = [];
5913
+ function findRows(n) {
5914
+ const role = n.role?.toLowerCase() ?? "";
5915
+ if (role === "columnheader" && n.name) {
5916
+ headers.push(n.name);
5917
+ }
5918
+ if (role === "row") {
5919
+ const cells = [];
5920
+ if (n.children) {
5921
+ for (const child of n.children) {
5922
+ const childRole = child.role?.toLowerCase() ?? "";
5923
+ if ((childRole === "cell" || childRole === "gridcell") && child.name) {
5924
+ cells.push(child.name);
5925
+ }
5926
+ }
5927
+ }
5928
+ if (cells.length > 0) rows.push(cells);
5929
+ }
5930
+ if (n.children) {
5931
+ for (const child of n.children) findRows(child);
5932
+ }
5933
+ }
5934
+ findRows(node);
5935
+ if (rows.length === 0) return null;
5936
+ return { headers, rows };
5937
+ }
5938
+ function extractKeyValueFromText(text) {
5939
+ const pairs = [];
5940
+ const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
5941
+ for (const line of lines) {
5942
+ const match = line.match(/^([A-Z][A-Za-z0-9 ]{1,30})[:—]\s+(.+)$/);
5943
+ if (match) {
5944
+ pairs.push({ key: match[1].trim(), value: match[2].trim() });
5945
+ }
5946
+ }
5947
+ return pairs.slice(0, 20);
5948
+ }
5949
+
5560
5950
  // src/browser/page.ts
5561
5951
  var DEFAULT_TIMEOUT2 = 3e4;
5952
+ function normalizeAXCheckedValue(value) {
5953
+ if (typeof value === "boolean") {
5954
+ return value;
5955
+ }
5956
+ if (typeof value === "string") {
5957
+ if (value === "true") return true;
5958
+ if (value === "false") return false;
5959
+ }
5960
+ return void 0;
5961
+ }
5562
5962
  var EVENT_LISTENER_TRACKER_SCRIPT = `(() => {
5563
5963
  if (globalThis.__bpEventListenerTrackerInstalled) return;
5564
5964
  Object.defineProperty(globalThis, '__bpEventListenerTrackerInstalled', {
@@ -7344,7 +7744,9 @@ var Page = class {
7344
7744
  }
7345
7745
  }
7346
7746
  const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
7347
- const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
7747
+ const checked = normalizeAXCheckedValue(
7748
+ node.properties?.find((p) => p.name === "checked")?.value.value
7749
+ );
7348
7750
  return {
7349
7751
  role,
7350
7752
  name,
@@ -7400,7 +7802,9 @@ var Page = class {
7400
7802
  const ref = nodeRefs.get(node.nodeId);
7401
7803
  const name = node.name?.value ?? "";
7402
7804
  const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
7403
- const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
7805
+ const checked = normalizeAXCheckedValue(
7806
+ node.properties?.find((p) => p.name === "checked")?.value.value
7807
+ );
7404
7808
  const value = node.value?.value;
7405
7809
  const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
7406
7810
  interactiveElements.push({
@@ -7467,6 +7871,45 @@ var Page = class {
7467
7871
  }
7468
7872
  }
7469
7873
  }
7874
+ // ============ Delta & Review ============
7875
+ /**
7876
+ * Capture current page state for delta comparison.
7877
+ * Call before an action, then call delta() again after and use computeDelta().
7878
+ */
7879
+ async captureState() {
7880
+ const [url, title, snapshot, forms, text] = await Promise.all([
7881
+ this.url(),
7882
+ this.title(),
7883
+ this.snapshot(),
7884
+ this.forms(),
7885
+ this.text()
7886
+ ]);
7887
+ return extractPageState(url, title, snapshot, forms, text);
7888
+ }
7889
+ /**
7890
+ * Compute what changed between two page states.
7891
+ * If no arguments: captures current state and returns it (for use as "before").
7892
+ * If one argument (before state): captures current state and computes delta.
7893
+ */
7894
+ async delta(before) {
7895
+ const currentState = await this.captureState();
7896
+ if (!before) return currentState;
7897
+ return computeDelta(before, currentState);
7898
+ }
7899
+ /**
7900
+ * Extract structured review surface from the current page.
7901
+ * Returns headings, form values, alerts, key-value pairs, tables, and status labels.
7902
+ */
7903
+ async review() {
7904
+ const [url, title, snapshot, forms, text] = await Promise.all([
7905
+ this.url(),
7906
+ this.title(),
7907
+ this.snapshot(),
7908
+ this.forms(),
7909
+ this.text()
7910
+ ]);
7911
+ return extractReview(url, title, snapshot, forms, text);
7912
+ }
7470
7913
  // ============ Batch Execution ============
7471
7914
  /**
7472
7915
  * Execute a batch of steps
@@ -8645,6 +9088,10 @@ function sleep4(ms) {
8645
9088
 
8646
9089
  export {
8647
9090
  pcmToWav,
9091
+ readString,
9092
+ readStringOr,
9093
+ formatConsoleArg,
9094
+ globToRegex,
8648
9095
  SENSITIVE_AUTOCOMPLETE_TOKENS,
8649
9096
  redactValueForRecording,
8650
9097
  fuzzyMatchElements,
@@ -8656,7 +9103,6 @@ export {
8656
9103
  normalizeTraceEvent,
8657
9104
  TRACE_BINDING_NAME,
8658
9105
  TRACE_SCRIPT,
8659
- LiveTraceCollector,
8660
9106
  addBatchToPage,
8661
9107
  validateSteps,
8662
9108
  grantAudioPermissions,