browser-pilot 0.0.6 → 0.0.8

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/actions.cjs CHANGED
@@ -25,6 +25,19 @@ __export(actions_exports, {
25
25
  });
26
26
  module.exports = __toCommonJS(actions_exports);
27
27
 
28
+ // src/browser/types.ts
29
+ var ElementNotFoundError = class extends Error {
30
+ selectors;
31
+ hints;
32
+ constructor(selectors, hints) {
33
+ const selectorList = Array.isArray(selectors) ? selectors : [selectors];
34
+ super(`Element not found: ${selectorList.join(", ")}`);
35
+ this.name = "ElementNotFoundError";
36
+ this.selectors = selectorList;
37
+ this.hints = hints;
38
+ }
39
+ };
40
+
28
41
  // src/actions/executor.ts
29
42
  var DEFAULT_TIMEOUT = 3e4;
30
43
  var BatchExecutor = class {
@@ -56,13 +69,15 @@ var BatchExecutor = class {
56
69
  });
57
70
  } catch (error) {
58
71
  const errorMessage = error instanceof Error ? error.message : String(error);
72
+ const hints = error instanceof ElementNotFoundError ? error.hints : void 0;
59
73
  results.push({
60
74
  index: i,
61
75
  action: step.action,
62
76
  selector: step.selector,
63
77
  success: false,
64
78
  durationMs: Date.now() - stepStart,
65
- error: errorMessage
79
+ error: errorMessage,
80
+ hints
66
81
  });
67
82
  if (onFail === "stop" && !step.optional) {
68
83
  return {
@@ -258,10 +273,12 @@ var BatchExecutor = class {
258
273
  }
259
274
  }
260
275
  /**
261
- * Get the first selector if multiple were provided
262
- * (actual used selector tracking would need to be implemented in Page)
276
+ * Get the actual selector that matched the element.
277
+ * Uses the last matched selector tracked by Page, falls back to first selector if unavailable.
263
278
  */
264
279
  getUsedSelector(selector) {
280
+ const matched = this.page.getLastMatchedSelector();
281
+ if (matched) return matched;
265
282
  return Array.isArray(selector) ? selector[0] : selector;
266
283
  }
267
284
  };
@@ -1,5 +1,5 @@
1
- import { P as Page, S as Step, B as BatchOptions, b as BatchResult } from './types-TVlTA7nH.cjs';
2
- export { A as ActionType, c as StepResult } from './types-TVlTA7nH.cjs';
1
+ import { P as Page, S as Step, B as BatchOptions, b as BatchResult } from './types-DklIxnbO.cjs';
2
+ export { A as ActionType, c as StepResult } from './types-DklIxnbO.cjs';
3
3
  import './client-7Nqka5MV.cjs';
4
4
 
5
5
  /**
@@ -18,8 +18,8 @@ declare class BatchExecutor {
18
18
  */
19
19
  private executeStep;
20
20
  /**
21
- * Get the first selector if multiple were provided
22
- * (actual used selector tracking would need to be implemented in Page)
21
+ * Get the actual selector that matched the element.
22
+ * Uses the last matched selector tracked by Page, falls back to first selector if unavailable.
23
23
  */
24
24
  private getUsedSelector;
25
25
  }
package/dist/actions.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as Page, S as Step, B as BatchOptions, b as BatchResult } from './types-CbdmaocU.js';
2
- export { A as ActionType, c as StepResult } from './types-CbdmaocU.js';
1
+ import { P as Page, S as Step, B as BatchOptions, b as BatchResult } from './types-Pv8KzZ6l.js';
2
+ export { A as ActionType, c as StepResult } from './types-Pv8KzZ6l.js';
3
3
  import './client-7Nqka5MV.js';
4
4
 
5
5
  /**
@@ -18,8 +18,8 @@ declare class BatchExecutor {
18
18
  */
19
19
  private executeStep;
20
20
  /**
21
- * Get the first selector if multiple were provided
22
- * (actual used selector tracking would need to be implemented in Page)
21
+ * Get the actual selector that matched the element.
22
+ * Uses the last matched selector tracked by Page, falls back to first selector if unavailable.
23
23
  */
24
24
  private getUsedSelector;
25
25
  }
package/dist/actions.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  BatchExecutor,
3
3
  addBatchToPage
4
- } from "./chunk-6RB3GKQP.mjs";
4
+ } from "./chunk-ZTQ37YQT.mjs";
5
5
  export {
6
6
  BatchExecutor,
7
7
  addBatchToPage
package/dist/browser.cjs CHANGED
@@ -457,6 +457,31 @@ function createProvider(options) {
457
457
  }
458
458
  }
459
459
 
460
+ // src/browser/types.ts
461
+ var ElementNotFoundError = class extends Error {
462
+ selectors;
463
+ hints;
464
+ constructor(selectors, hints) {
465
+ const selectorList = Array.isArray(selectors) ? selectors : [selectors];
466
+ super(`Element not found: ${selectorList.join(", ")}`);
467
+ this.name = "ElementNotFoundError";
468
+ this.selectors = selectorList;
469
+ this.hints = hints;
470
+ }
471
+ };
472
+ var TimeoutError = class extends Error {
473
+ constructor(message = "Operation timed out") {
474
+ super(message);
475
+ this.name = "TimeoutError";
476
+ }
477
+ };
478
+ var NavigationError = class extends Error {
479
+ constructor(message) {
480
+ super(message);
481
+ this.name = "NavigationError";
482
+ }
483
+ };
484
+
460
485
  // src/actions/executor.ts
461
486
  var DEFAULT_TIMEOUT = 3e4;
462
487
  var BatchExecutor = class {
@@ -488,13 +513,15 @@ var BatchExecutor = class {
488
513
  });
489
514
  } catch (error) {
490
515
  const errorMessage = error instanceof Error ? error.message : String(error);
516
+ const hints = error instanceof ElementNotFoundError ? error.hints : void 0;
491
517
  results.push({
492
518
  index: i,
493
519
  action: step.action,
494
520
  selector: step.selector,
495
521
  success: false,
496
522
  durationMs: Date.now() - stepStart,
497
- error: errorMessage
523
+ error: errorMessage,
524
+ hints
498
525
  });
499
526
  if (onFail === "stop" && !step.optional) {
500
527
  return {
@@ -690,10 +717,12 @@ var BatchExecutor = class {
690
717
  }
691
718
  }
692
719
  /**
693
- * Get the first selector if multiple were provided
694
- * (actual used selector tracking would need to be implemented in Page)
720
+ * Get the actual selector that matched the element.
721
+ * Uses the last matched selector tracked by Page, falls back to first selector if unavailable.
695
722
  */
696
723
  getUsedSelector(selector) {
724
+ const matched = this.page.getLastMatchedSelector();
725
+ if (matched) return matched;
697
726
  return Array.isArray(selector) ? selector[0] : selector;
698
727
  }
699
728
  };
@@ -1085,33 +1114,256 @@ async function waitForNetworkIdle(cdp, options = {}) {
1085
1114
  });
1086
1115
  }
1087
1116
 
1088
- // src/browser/types.ts
1089
- var ElementNotFoundError = class extends Error {
1090
- selectors;
1091
- constructor(selectors) {
1092
- const selectorList = Array.isArray(selectors) ? selectors : [selectors];
1093
- super(`Element not found: ${selectorList.join(", ")}`);
1094
- this.name = "ElementNotFoundError";
1095
- this.selectors = selectorList;
1117
+ // src/browser/fuzzy-match.ts
1118
+ function jaroWinkler(a, b) {
1119
+ if (a.length === 0 && b.length === 0) return 0;
1120
+ if (a.length === 0 || b.length === 0) return 0;
1121
+ if (a === b) return 1;
1122
+ const s1 = a.toLowerCase();
1123
+ const s2 = b.toLowerCase();
1124
+ const matchWindow = Math.max(0, Math.floor(Math.max(s1.length, s2.length) / 2) - 1);
1125
+ const s1Matches = new Array(s1.length).fill(false);
1126
+ const s2Matches = new Array(s2.length).fill(false);
1127
+ let matches = 0;
1128
+ let transpositions = 0;
1129
+ for (let i = 0; i < s1.length; i++) {
1130
+ const start = Math.max(0, i - matchWindow);
1131
+ const end = Math.min(i + matchWindow + 1, s2.length);
1132
+ for (let j = start; j < end; j++) {
1133
+ if (s2Matches[j] || s1[i] !== s2[j]) continue;
1134
+ s1Matches[i] = true;
1135
+ s2Matches[j] = true;
1136
+ matches++;
1137
+ break;
1138
+ }
1139
+ }
1140
+ if (matches === 0) return 0;
1141
+ let k = 0;
1142
+ for (let i = 0; i < s1.length; i++) {
1143
+ if (!s1Matches[i]) continue;
1144
+ while (!s2Matches[k]) k++;
1145
+ if (s1[i] !== s2[k]) transpositions++;
1146
+ k++;
1147
+ }
1148
+ const jaro = (matches / s1.length + matches / s2.length + (matches - transpositions / 2) / matches) / 3;
1149
+ let prefix = 0;
1150
+ for (let i = 0; i < Math.min(4, Math.min(s1.length, s2.length)); i++) {
1151
+ if (s1[i] === s2[i]) {
1152
+ prefix++;
1153
+ } else {
1154
+ break;
1155
+ }
1096
1156
  }
1097
- };
1098
- var TimeoutError = class extends Error {
1099
- constructor(message = "Operation timed out") {
1100
- super(message);
1101
- this.name = "TimeoutError";
1157
+ const WINKLER_SCALING = 0.1;
1158
+ return jaro + prefix * WINKLER_SCALING * (1 - jaro);
1159
+ }
1160
+ function stringSimilarity(a, b) {
1161
+ if (a.length === 0 || b.length === 0) return 0;
1162
+ const lowerA = a.toLowerCase();
1163
+ const lowerB = b.toLowerCase();
1164
+ if (lowerA === lowerB) return 1;
1165
+ const jw = jaroWinkler(a, b);
1166
+ let containsBonus = 0;
1167
+ if (lowerB.includes(lowerA)) {
1168
+ containsBonus = 0.2;
1169
+ } else if (lowerA.includes(lowerB)) {
1170
+ containsBonus = 0.1;
1171
+ }
1172
+ return Math.min(1, jw + containsBonus);
1173
+ }
1174
+ function scoreElement(query, element) {
1175
+ const lowerQuery = query.toLowerCase();
1176
+ const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
1177
+ let nameScore = 0;
1178
+ if (element.name) {
1179
+ const lowerName = element.name.toLowerCase();
1180
+ if (lowerName === lowerQuery) {
1181
+ nameScore = 1;
1182
+ } else if (lowerName.includes(lowerQuery)) {
1183
+ nameScore = 0.8;
1184
+ } else if (words.length > 0) {
1185
+ const matchedWords = words.filter((w) => lowerName.includes(w));
1186
+ nameScore = matchedWords.length / words.length * 0.7;
1187
+ } else {
1188
+ nameScore = stringSimilarity(query, element.name) * 0.6;
1189
+ }
1102
1190
  }
1103
- };
1104
- var NavigationError = class extends Error {
1105
- constructor(message) {
1106
- super(message);
1107
- this.name = "NavigationError";
1191
+ let roleScore = 0;
1192
+ const lowerRole = element.role.toLowerCase();
1193
+ if (lowerRole === lowerQuery || lowerQuery.includes(lowerRole)) {
1194
+ roleScore = 0.3;
1195
+ } else if (words.some((w) => lowerRole.includes(w))) {
1196
+ roleScore = 0.2;
1108
1197
  }
1198
+ let selectorScore = 0;
1199
+ const lowerSelector = element.selector.toLowerCase();
1200
+ if (words.some((w) => lowerSelector.includes(w))) {
1201
+ selectorScore = 0.2;
1202
+ }
1203
+ const totalScore = nameScore * 0.6 + roleScore * 0.25 + selectorScore * 0.15;
1204
+ return totalScore;
1205
+ }
1206
+ function explainMatch(query, element, score) {
1207
+ const reasons = [];
1208
+ const lowerQuery = query.toLowerCase();
1209
+ const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
1210
+ if (element.name) {
1211
+ const lowerName = element.name.toLowerCase();
1212
+ if (lowerName === lowerQuery) {
1213
+ reasons.push("exact name match");
1214
+ } else if (lowerName.includes(lowerQuery)) {
1215
+ reasons.push("name contains query");
1216
+ } else if (words.some((w) => lowerName.includes(w))) {
1217
+ const matchedWords = words.filter((w) => lowerName.includes(w));
1218
+ reasons.push(`name contains: ${matchedWords.join(", ")}`);
1219
+ } else if (stringSimilarity(query, element.name) > 0.5) {
1220
+ reasons.push("similar name");
1221
+ }
1222
+ }
1223
+ const lowerRole = element.role.toLowerCase();
1224
+ if (lowerRole === lowerQuery || words.some((w) => w === lowerRole)) {
1225
+ reasons.push(`role: ${element.role}`);
1226
+ }
1227
+ if (words.some((w) => element.selector.toLowerCase().includes(w))) {
1228
+ reasons.push("selector match");
1229
+ }
1230
+ if (reasons.length === 0) {
1231
+ reasons.push(`fuzzy match (score: ${score.toFixed(2)})`);
1232
+ }
1233
+ return reasons.join(", ");
1234
+ }
1235
+ function fuzzyMatchElements(query, elements, maxResults = 5) {
1236
+ if (!query || query.length === 0) {
1237
+ return [];
1238
+ }
1239
+ const THRESHOLD = 0.3;
1240
+ const scored = elements.map((element) => ({
1241
+ element,
1242
+ score: scoreElement(query, element)
1243
+ }));
1244
+ return scored.filter((s) => s.score >= THRESHOLD).sort((a, b) => b.score - a.score).slice(0, maxResults).map((s) => ({
1245
+ element: s.element,
1246
+ score: s.score,
1247
+ matchReason: explainMatch(query, s.element, s.score)
1248
+ }));
1249
+ }
1250
+
1251
+ // src/browser/hint-generator.ts
1252
+ var ACTION_ROLE_MAP = {
1253
+ click: ["button", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "tab", "option"],
1254
+ fill: ["textbox", "searchbox", "textarea"],
1255
+ type: ["textbox", "searchbox", "textarea"],
1256
+ submit: ["button", "form"],
1257
+ select: ["combobox", "listbox", "option"],
1258
+ check: ["checkbox", "radio", "switch"],
1259
+ uncheck: ["checkbox", "switch"],
1260
+ focus: [],
1261
+ // Any focusable element
1262
+ hover: [],
1263
+ // Any element
1264
+ clear: ["textbox", "searchbox", "textarea"]
1109
1265
  };
1266
+ function extractIntent(selectors) {
1267
+ const patterns = [];
1268
+ let text = "";
1269
+ for (const selector of selectors) {
1270
+ if (selector.startsWith("ref:")) {
1271
+ continue;
1272
+ }
1273
+ const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
1274
+ if (idMatch) {
1275
+ patterns.push(idMatch[1]);
1276
+ }
1277
+ const ariaMatch = selector.match(/\[aria-label=["']([^"']+)["']\]/);
1278
+ if (ariaMatch) {
1279
+ patterns.push(ariaMatch[1]);
1280
+ }
1281
+ const testidMatch = selector.match(/\[data-testid=["']([^"']+)["']\]/);
1282
+ if (testidMatch) {
1283
+ patterns.push(testidMatch[1]);
1284
+ }
1285
+ const classMatch = selector.match(/\.([a-zA-Z0-9_-]+)/);
1286
+ if (classMatch) {
1287
+ patterns.push(classMatch[1]);
1288
+ }
1289
+ }
1290
+ patterns.sort((a, b) => b.length - a.length);
1291
+ text = patterns[0] ?? selectors[0] ?? "";
1292
+ return { text, patterns };
1293
+ }
1294
+ function getHintType(selector) {
1295
+ if (selector.startsWith("ref:")) return "ref";
1296
+ if (selector.includes("data-testid")) return "testid";
1297
+ if (selector.includes("aria-label")) return "aria";
1298
+ if (selector.startsWith("#")) return "id";
1299
+ return "css";
1300
+ }
1301
+ function getConfidence(score) {
1302
+ if (score >= 0.8) return "high";
1303
+ if (score >= 0.5) return "medium";
1304
+ return "low";
1305
+ }
1306
+ function diversifyHints(candidates, maxHints) {
1307
+ const hints = [];
1308
+ const usedTypes = /* @__PURE__ */ new Set();
1309
+ for (const candidate of candidates) {
1310
+ if (hints.length >= maxHints) break;
1311
+ const refSelector = `ref:${candidate.element.ref}`;
1312
+ const hintType = getHintType(refSelector);
1313
+ if (!usedTypes.has(hintType)) {
1314
+ hints.push({
1315
+ selector: refSelector,
1316
+ reason: candidate.matchReason,
1317
+ confidence: getConfidence(candidate.score),
1318
+ element: {
1319
+ ref: candidate.element.ref,
1320
+ role: candidate.element.role,
1321
+ name: candidate.element.name,
1322
+ disabled: candidate.element.disabled
1323
+ }
1324
+ });
1325
+ usedTypes.add(hintType);
1326
+ } else if (hints.length < maxHints) {
1327
+ hints.push({
1328
+ selector: refSelector,
1329
+ reason: candidate.matchReason,
1330
+ confidence: getConfidence(candidate.score),
1331
+ element: {
1332
+ ref: candidate.element.ref,
1333
+ role: candidate.element.role,
1334
+ name: candidate.element.name,
1335
+ disabled: candidate.element.disabled
1336
+ }
1337
+ });
1338
+ }
1339
+ }
1340
+ return hints;
1341
+ }
1342
+ async function generateHints(page, failedSelectors, actionType, maxHints = 3) {
1343
+ let snapshot;
1344
+ try {
1345
+ snapshot = await page.snapshot();
1346
+ } catch {
1347
+ return [];
1348
+ }
1349
+ const intent = extractIntent(failedSelectors);
1350
+ const roleFilter = ACTION_ROLE_MAP[actionType] ?? [];
1351
+ let candidates = snapshot.interactiveElements;
1352
+ if (roleFilter.length > 0) {
1353
+ candidates = candidates.filter((el) => roleFilter.includes(el.role));
1354
+ }
1355
+ const matches = fuzzyMatchElements(intent.text, candidates, maxHints * 2);
1356
+ if (matches.length === 0) {
1357
+ return [];
1358
+ }
1359
+ return diversifyHints(matches, maxHints);
1360
+ }
1110
1361
 
1111
1362
  // src/browser/page.ts
1112
1363
  var DEFAULT_TIMEOUT2 = 3e4;
1113
1364
  var Page = class {
1114
1365
  cdp;
1366
+ _targetId;
1115
1367
  rootNodeId = null;
1116
1368
  batchExecutor;
1117
1369
  emulationState = {};
@@ -1130,10 +1382,19 @@ var Page = class {
1130
1382
  frameExecutionContexts = /* @__PURE__ */ new Map();
1131
1383
  /** Current frame's execution context ID (null = main frame default) */
1132
1384
  currentFrameContextId = null;
1133
- constructor(cdp) {
1385
+ /** Last matched selector from findElement (for selectorUsed tracking) */
1386
+ _lastMatchedSelector;
1387
+ constructor(cdp, targetId) {
1134
1388
  this.cdp = cdp;
1389
+ this._targetId = targetId;
1135
1390
  this.batchExecutor = new BatchExecutor(this);
1136
1391
  }
1392
+ /**
1393
+ * Get the CDP target ID for this page
1394
+ */
1395
+ get targetId() {
1396
+ return this._targetId;
1397
+ }
1137
1398
  /**
1138
1399
  * Get the underlying CDP client for advanced operations.
1139
1400
  * Use with caution - prefer high-level Page methods when possible.
@@ -1141,6 +1402,13 @@ var Page = class {
1141
1402
  get cdpClient() {
1142
1403
  return this.cdp;
1143
1404
  }
1405
+ /**
1406
+ * Get the last matched selector from findElement (for selectorUsed tracking).
1407
+ * Returns undefined if no selector has been matched yet.
1408
+ */
1409
+ getLastMatchedSelector() {
1410
+ return this._lastMatchedSelector;
1411
+ }
1144
1412
  /**
1145
1413
  * Initialize the page (enable required CDP domains)
1146
1414
  */
@@ -1260,7 +1528,9 @@ var Page = class {
1260
1528
  const element = await this.findElement(selector, options);
1261
1529
  if (!element) {
1262
1530
  if (options.optional) return false;
1263
- throw new ElementNotFoundError(selector);
1531
+ const selectorList = Array.isArray(selector) ? selector : [selector];
1532
+ const hints = await generateHints(this, selectorList, "click");
1533
+ throw new ElementNotFoundError(selector, hints);
1264
1534
  }
1265
1535
  await this.scrollIntoView(element.nodeId);
1266
1536
  const submitResult = await this.evaluateInFrame(
@@ -1296,7 +1566,9 @@ var Page = class {
1296
1566
  const element = await this.findElement(selector, options);
1297
1567
  if (!element) {
1298
1568
  if (options.optional) return false;
1299
- throw new ElementNotFoundError(selector);
1569
+ const selectorList = Array.isArray(selector) ? selector : [selector];
1570
+ const hints = await generateHints(this, selectorList, "fill");
1571
+ throw new ElementNotFoundError(selector, hints);
1300
1572
  }
1301
1573
  await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
1302
1574
  if (clear) {
@@ -1374,7 +1646,9 @@ var Page = class {
1374
1646
  const element = await this.findElement(selector, options);
1375
1647
  if (!element) {
1376
1648
  if (options.optional) return false;
1377
- throw new ElementNotFoundError(selector);
1649
+ const selectorList = Array.isArray(selector) ? selector : [selector];
1650
+ const hints = await generateHints(this, selectorList, "select");
1651
+ throw new ElementNotFoundError(selector, hints);
1378
1652
  }
1379
1653
  const values = Array.isArray(value) ? value : [value];
1380
1654
  await this.cdp.send("Runtime.evaluate", {
@@ -1435,7 +1709,9 @@ var Page = class {
1435
1709
  const element = await this.findElement(selector, options);
1436
1710
  if (!element) {
1437
1711
  if (options.optional) return false;
1438
- throw new ElementNotFoundError(selector);
1712
+ const selectorList = Array.isArray(selector) ? selector : [selector];
1713
+ const hints = await generateHints(this, selectorList, "check");
1714
+ throw new ElementNotFoundError(selector, hints);
1439
1715
  }
1440
1716
  const result = await this.cdp.send("Runtime.evaluate", {
1441
1717
  expression: `(() => {
@@ -1455,7 +1731,9 @@ var Page = class {
1455
1731
  const element = await this.findElement(selector, options);
1456
1732
  if (!element) {
1457
1733
  if (options.optional) return false;
1458
- throw new ElementNotFoundError(selector);
1734
+ const selectorList = Array.isArray(selector) ? selector : [selector];
1735
+ const hints = await generateHints(this, selectorList, "uncheck");
1736
+ throw new ElementNotFoundError(selector, hints);
1459
1737
  }
1460
1738
  const result = await this.cdp.send("Runtime.evaluate", {
1461
1739
  expression: `(() => {
@@ -1475,13 +1753,40 @@ var Page = class {
1475
1753
  * - 'auto' (default): Attempt to detect navigation for 1 second, then assume client-side handling
1476
1754
  * - true: Wait for full navigation (traditional forms)
1477
1755
  * - false: Return immediately (AJAX forms where you'll wait for something else)
1756
+ *
1757
+ * When targeting a <form> element directly, uses form.requestSubmit() which fires
1758
+ * the submit event and triggers HTML5 validation.
1478
1759
  */
1479
1760
  async submit(selector, options = {}) {
1480
1761
  const { method = "enter+click", waitForNavigation: shouldWait = "auto" } = options;
1481
1762
  const element = await this.findElement(selector, options);
1482
1763
  if (!element) {
1483
1764
  if (options.optional) return false;
1484
- throw new ElementNotFoundError(selector);
1765
+ const selectorList = Array.isArray(selector) ? selector : [selector];
1766
+ const hints = await generateHints(this, selectorList, "submit");
1767
+ throw new ElementNotFoundError(selector, hints);
1768
+ }
1769
+ const isFormElement = await this.evaluateInFrame(
1770
+ `(() => {
1771
+ const el = document.querySelector(${JSON.stringify(element.selector)});
1772
+ return el instanceof HTMLFormElement;
1773
+ })()`
1774
+ );
1775
+ if (isFormElement.result.value) {
1776
+ await this.evaluateInFrame(
1777
+ `(() => {
1778
+ const form = document.querySelector(${JSON.stringify(element.selector)});
1779
+ if (form && form instanceof HTMLFormElement) {
1780
+ form.requestSubmit();
1781
+ }
1782
+ })()`
1783
+ );
1784
+ if (shouldWait === true) {
1785
+ await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
1786
+ } else if (shouldWait === "auto") {
1787
+ await Promise.race([this.waitForNavigation({ timeout: 1e3, optional: true }), sleep2(500)]);
1788
+ }
1789
+ return true;
1485
1790
  }
1486
1791
  await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
1487
1792
  if (method.includes("enter")) {
@@ -1552,7 +1857,9 @@ var Page = class {
1552
1857
  const element = await this.findElement(selector, options);
1553
1858
  if (!element) {
1554
1859
  if (options.optional) return false;
1555
- throw new ElementNotFoundError(selector);
1860
+ const selectorList = Array.isArray(selector) ? selector : [selector];
1861
+ const hints = await generateHints(this, selectorList, "focus");
1862
+ throw new ElementNotFoundError(selector, hints);
1556
1863
  }
1557
1864
  await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
1558
1865
  return true;
@@ -1565,7 +1872,9 @@ var Page = class {
1565
1872
  const element = await this.findElement(selector, options);
1566
1873
  if (!element) {
1567
1874
  if (options.optional) return false;
1568
- throw new ElementNotFoundError(selector);
1875
+ const selectorList = Array.isArray(selector) ? selector : [selector];
1876
+ const hints = await generateHints(this, selectorList, "hover");
1877
+ throw new ElementNotFoundError(selector, hints);
1569
1878
  }
1570
1879
  await this.scrollIntoView(element.nodeId);
1571
1880
  const box = await this.getBoxModel(element.nodeId);
@@ -2520,6 +2829,7 @@ var Page = class {
2520
2829
  async findElement(selectors, options = {}) {
2521
2830
  const { timeout = DEFAULT_TIMEOUT2 } = options;
2522
2831
  const selectorList = Array.isArray(selectors) ? selectors : [selectors];
2832
+ this._lastMatchedSelector = void 0;
2523
2833
  for (const selector of selectorList) {
2524
2834
  if (selector.startsWith("ref:")) {
2525
2835
  const ref = selector.slice(4);
@@ -2536,6 +2846,7 @@ var Page = class {
2536
2846
  }
2537
2847
  );
2538
2848
  if (pushResult.nodeIds?.[0]) {
2849
+ this._lastMatchedSelector = selector;
2539
2850
  return {
2540
2851
  nodeId: pushResult.nodeIds[0],
2541
2852
  backendNodeId,
@@ -2569,6 +2880,7 @@ var Page = class {
2569
2880
  "DOM.describeNode",
2570
2881
  { nodeId: queryResult.nodeId }
2571
2882
  );
2883
+ this._lastMatchedSelector = result.selector;
2572
2884
  return {
2573
2885
  nodeId: queryResult.nodeId,
2574
2886
  backendNodeId: describeResult2.node.backendNodeId,
@@ -2596,6 +2908,7 @@ var Page = class {
2596
2908
  "DOM.describeNode",
2597
2909
  { nodeId: nodeResult.nodeId }
2598
2910
  );
2911
+ this._lastMatchedSelector = result.selector;
2599
2912
  return {
2600
2913
  nodeId: nodeResult.nodeId,
2601
2914
  backendNodeId: describeResult.node.backendNodeId,
@@ -2702,14 +3015,24 @@ var Browser = class _Browser {
2702
3015
  * Get or create a page by name
2703
3016
  * If no name is provided, returns the first available page or creates a new one
2704
3017
  */
2705
- async page(name) {
3018
+ async page(name, options) {
2706
3019
  const pageName = name ?? "default";
2707
3020
  const cached = this.pages.get(pageName);
2708
3021
  if (cached) return cached;
2709
3022
  const targets = await this.cdp.send("Target.getTargets");
2710
3023
  const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
2711
3024
  let targetId;
2712
- if (pageTargets.length > 0) {
3025
+ if (options?.targetId) {
3026
+ const targetExists = pageTargets.some((t) => t.targetId === options.targetId);
3027
+ if (targetExists) {
3028
+ targetId = options.targetId;
3029
+ } else {
3030
+ console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
3031
+ targetId = pageTargets.length > 0 ? pageTargets[0].targetId : (await this.cdp.send("Target.createTarget", {
3032
+ url: "about:blank"
3033
+ })).targetId;
3034
+ }
3035
+ } else if (pageTargets.length > 0) {
2713
3036
  targetId = pageTargets[0].targetId;
2714
3037
  } else {
2715
3038
  const result = await this.cdp.send("Target.createTarget", {
@@ -2718,7 +3041,7 @@ var Browser = class _Browser {
2718
3041
  targetId = result.targetId;
2719
3042
  }
2720
3043
  await this.cdp.attachToTarget(targetId);
2721
- const page = new Page(this.cdp);
3044
+ const page = new Page(this.cdp, targetId);
2722
3045
  await page.init();
2723
3046
  this.pages.set(pageName, page);
2724
3047
  return page;
@@ -2731,7 +3054,7 @@ var Browser = class _Browser {
2731
3054
  url
2732
3055
  });
2733
3056
  await this.cdp.attachToTarget(result.targetId);
2734
- const page = new Page(this.cdp);
3057
+ const page = new Page(this.cdp, result.targetId);
2735
3058
  await page.init();
2736
3059
  const name = `page-${this.pages.size + 1}`;
2737
3060
  this.pages.set(name, page);