@agentium/browser 2.0.7 → 2.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/index.cjs CHANGED
@@ -150,7 +150,11 @@ function buildStealthContextOpts(config, viewport) {
150
150
  locale: config.locale ?? "en-US",
151
151
  timezoneId: config.timezone ?? "America/New_York",
152
152
  colorScheme: "light",
153
- deviceScaleFactor: 2,
153
+ // Default to 1 — matches the most common real-user setup and avoids
154
+ // visual zoom/stretch in headed mode on non-Retina displays (the host
155
+ // OS would downsample a 2× rendered surface). Users on Retina-only
156
+ // deployments can opt back into DPR=2 for sharper screenshots.
157
+ deviceScaleFactor: config.deviceScaleFactor ?? 1,
154
158
  hasTouch: false,
155
159
  javaScriptEnabled: true,
156
160
  ignoreHTTPSErrors: config.ignoreHTTPSErrors ?? false
@@ -342,7 +346,24 @@ var BrowserProvider = class {
342
346
  "[contenteditable='true']",
343
347
  "[tabindex]:not([tabindex='-1'])"
344
348
  ];
345
- const all = Array.from(doc.querySelectorAll(selectors.join(",")));
349
+ const semantic = Array.from(doc.querySelectorAll(selectors.join(",")));
350
+ const POINTER_SCAN_CAP = 2e3;
351
+ const pointerCandidates = [];
352
+ const allEls = doc.querySelectorAll("*");
353
+ for (let i = 0; i < allEls.length && pointerCandidates.length < POINTER_SCAN_CAP; i++) {
354
+ const el = allEls[i];
355
+ if (!el) continue;
356
+ const tag = el.tagName.toLowerCase();
357
+ if (tag === "html" || tag === "body" || tag === "head" || tag === "script" || tag === "style") continue;
358
+ const rect = el.getBoundingClientRect();
359
+ if (rect.width < 4 || rect.height < 4) continue;
360
+ if (rect.bottom < 0 || rect.top > win.innerHeight) continue;
361
+ if (rect.right < 0 || rect.left > win.innerWidth) continue;
362
+ const style = win.getComputedStyle(el);
363
+ if (style.cursor !== "pointer") continue;
364
+ pointerCandidates.push(el);
365
+ }
366
+ const all = semantic.concat(pointerCandidates);
346
367
  const vh = win.innerHeight;
347
368
  const vw = win.innerWidth;
348
369
  const entries = [];
@@ -364,6 +385,7 @@ var BrowserProvider = class {
364
385
  if (hit && hit !== el && !el.contains(hit) && !hit.contains(el)) {
365
386
  continue;
366
387
  }
388
+ if (hit && el.contains(hit)) seen.add(hit);
367
389
  } catch {
368
390
  }
369
391
  seen.add(el);
@@ -392,6 +414,29 @@ var BrowserProvider = class {
392
414
  }, max);
393
415
  return elements;
394
416
  }
417
+ /**
418
+ * Deterministic, DOM-based click using Playwright's text locator.
419
+ *
420
+ * Returns `true` if a matching, visible, clickable element was found and
421
+ * clicked within `timeout` ms; `false` otherwise (so the caller can fall
422
+ * back to coordinate clicking). Substring-matches by default — e.g.
423
+ * `clickByText("Cheapest")` matches "Cheapest · 23-28 days · $2,550".
424
+ *
425
+ * This is the same pattern browser-use uses in DOM-only mode and is far
426
+ * more reliable than coordinate clicks for text-bearing targets.
427
+ */
428
+ async clickByText(keyword, opts) {
429
+ this.ensurePage();
430
+ const timeout = opts?.timeout ?? 3e3;
431
+ try {
432
+ const locator = this.page.locator(`text=${keyword}`).first();
433
+ await locator.click({ timeout });
434
+ await this.humanPause();
435
+ return true;
436
+ } catch {
437
+ return false;
438
+ }
439
+ }
395
440
  // ── Page Info ────────────────────────────────────────────────────────
396
441
  async getPageInfo() {
397
442
  this.ensurePage();
@@ -564,6 +609,7 @@ function buildSystemPrompt(viewport, extraInstructions, credentialKeys) {
564
609
  `### click`,
565
610
  `Click at a specific coordinate. Use for buttons, links, inputs, checkboxes, etc.`,
566
611
  `\`{ "action": "click", "x": <number>, "y": <number>, "description": "<what you are clicking>" }\``,
612
+ `IMPORTANT: when the target has visible text, ALWAYS put that text in single or double quotes inside the description, e.g. \`"description": "Click on 'Cheapest' tab"\` or \`"description": "Press the \\"Sign in\\" button"\`. The runtime uses the quoted phrase to do a deterministic DOM text-locator click and only falls back to (x,y) if no match is found, which dramatically improves accuracy on dynamic pages.`,
567
613
  ``,
568
614
  `### type`,
569
615
  `Type text. If x/y are provided, click that position first (to focus the input), then type. If omitted, types into the currently focused element. To press Enter after typing (e.g., to submit a search), append "\\n" to the text.`,
@@ -968,9 +1014,20 @@ ${memoryContext}` : memoryContext;
968
1014
  async executeAction(browser, action) {
969
1015
  try {
970
1016
  switch (action.action) {
971
- case "click":
972
- await browser.click(action.x, action.y);
1017
+ case "click": {
1018
+ const keyword = this.extractClickKeyword(action.description);
1019
+ let clicked = false;
1020
+ if (keyword) {
1021
+ clicked = await browser.clickByText(keyword);
1022
+ if (clicked) {
1023
+ this.logger.debug("Clicked by text", { keyword });
1024
+ }
1025
+ }
1026
+ if (!clicked) {
1027
+ await browser.click(action.x, action.y);
1028
+ }
973
1029
  break;
1030
+ }
974
1031
  case "type": {
975
1032
  const resolvedText = this.credentials ? this.credentials.resolve(action.text) : action.text;
976
1033
  if (action.x != null && action.y != null) {
@@ -1007,6 +1064,52 @@ ${memoryContext}` : memoryContext;
1007
1064
  sleep(ms) {
1008
1065
  return new Promise((resolve) => setTimeout(resolve, ms));
1009
1066
  }
1067
+ /**
1068
+ * Parse a quoted target keyword from a click action's `description`.
1069
+ *
1070
+ * The system prompt asks the model to include the visible label of its
1071
+ * click target in quotes (e.g. `"Click on 'Cheapest' tab"`). When a usable
1072
+ * quoted phrase is present we return it so `executeAction` can try a
1073
+ * deterministic Playwright text locator before falling back to
1074
+ * coordinate clicking.
1075
+ *
1076
+ * Returns `undefined` for generic / ambiguous labels (login buttons,
1077
+ * close, OK, etc.) where a substring text match could trivially fire on
1078
+ * the wrong element.
1079
+ */
1080
+ extractClickKeyword(description) {
1081
+ if (!description) return void 0;
1082
+ const match = description.match(/['"\u2018\u2019\u201C\u201D]([^'"\u2018\u2019\u201C\u201D]{1,80})['"\u2018\u2019\u201C\u201D]/);
1083
+ if (!match) return void 0;
1084
+ const keyword = match[1].trim();
1085
+ if (!keyword || keyword.length < 2) return void 0;
1086
+ const skip = /* @__PURE__ */ new Set([
1087
+ "log in",
1088
+ "login",
1089
+ "sign in",
1090
+ "sign up",
1091
+ "submit",
1092
+ "close",
1093
+ "ok",
1094
+ "okay",
1095
+ "cancel",
1096
+ "yes",
1097
+ "no",
1098
+ "x",
1099
+ "continue",
1100
+ "next",
1101
+ "back",
1102
+ "accept",
1103
+ "dismiss",
1104
+ "got it",
1105
+ "agree",
1106
+ "i agree",
1107
+ "allow",
1108
+ "deny"
1109
+ ]);
1110
+ if (skip.has(keyword.toLowerCase())) return void 0;
1111
+ return keyword;
1112
+ }
1010
1113
  };
1011
1114
 
1012
1115
  // src/credential-vault.ts
package/dist/index.d.cts CHANGED
@@ -198,6 +198,13 @@ interface StealthConfig {
198
198
  };
199
199
  /** Ignore HTTPS certificate errors. Default: false (secure). Only enable for local testing. */
200
200
  ignoreHTTPSErrors?: boolean;
201
+ /**
202
+ * `window.devicePixelRatio` to emulate. Default: `1`. Set to `2` to mimic
203
+ * a Retina display (sharper screenshots at 2× cost). Setting to 2 on a
204
+ * non-Retina host display can cause the headed window to look zoomed-out
205
+ * or stretched because the OS compositor downsamples a 2× surface.
206
+ */
207
+ deviceScaleFactor?: number;
201
208
  /** HTTP/SOCKS proxy. Format: "http://user:pass@host:port" */
202
209
  proxy?: {
203
210
  server: string;
@@ -259,6 +266,20 @@ declare class BrowserAgent {
259
266
  private finalize;
260
267
  private executeAction;
261
268
  private sleep;
269
+ /**
270
+ * Parse a quoted target keyword from a click action's `description`.
271
+ *
272
+ * The system prompt asks the model to include the visible label of its
273
+ * click target in quotes (e.g. `"Click on 'Cheapest' tab"`). When a usable
274
+ * quoted phrase is present we return it so `executeAction` can try a
275
+ * deterministic Playwright text locator before falling back to
276
+ * coordinate clicking.
277
+ *
278
+ * Returns `undefined` for generic / ambiguous labels (login buttons,
279
+ * close, OK, etc.) where a substring text match could trivially fire on
280
+ * the wrong element.
281
+ */
282
+ private extractClickKeyword;
262
283
  }
263
284
 
264
285
  /**
@@ -305,6 +326,20 @@ declare class BrowserProvider {
305
326
  extractDOM(opts?: {
306
327
  maxElements?: number;
307
328
  }): Promise<string>;
329
+ /**
330
+ * Deterministic, DOM-based click using Playwright's text locator.
331
+ *
332
+ * Returns `true` if a matching, visible, clickable element was found and
333
+ * clicked within `timeout` ms; `false` otherwise (so the caller can fall
334
+ * back to coordinate clicking). Substring-matches by default — e.g.
335
+ * `clickByText("Cheapest")` matches "Cheapest · 23-28 days · $2,550".
336
+ *
337
+ * This is the same pattern browser-use uses in DOM-only mode and is far
338
+ * more reliable than coordinate clicks for text-bearing targets.
339
+ */
340
+ clickByText(keyword: string, opts?: {
341
+ timeout?: number;
342
+ }): Promise<boolean>;
308
343
  getPageInfo(): Promise<PageInfo>;
309
344
  waitForStable(minWait?: number): Promise<void>;
310
345
  newTab(url?: string): Promise<string>;
package/dist/index.d.ts CHANGED
@@ -198,6 +198,13 @@ interface StealthConfig {
198
198
  };
199
199
  /** Ignore HTTPS certificate errors. Default: false (secure). Only enable for local testing. */
200
200
  ignoreHTTPSErrors?: boolean;
201
+ /**
202
+ * `window.devicePixelRatio` to emulate. Default: `1`. Set to `2` to mimic
203
+ * a Retina display (sharper screenshots at 2× cost). Setting to 2 on a
204
+ * non-Retina host display can cause the headed window to look zoomed-out
205
+ * or stretched because the OS compositor downsamples a 2× surface.
206
+ */
207
+ deviceScaleFactor?: number;
201
208
  /** HTTP/SOCKS proxy. Format: "http://user:pass@host:port" */
202
209
  proxy?: {
203
210
  server: string;
@@ -259,6 +266,20 @@ declare class BrowserAgent {
259
266
  private finalize;
260
267
  private executeAction;
261
268
  private sleep;
269
+ /**
270
+ * Parse a quoted target keyword from a click action's `description`.
271
+ *
272
+ * The system prompt asks the model to include the visible label of its
273
+ * click target in quotes (e.g. `"Click on 'Cheapest' tab"`). When a usable
274
+ * quoted phrase is present we return it so `executeAction` can try a
275
+ * deterministic Playwright text locator before falling back to
276
+ * coordinate clicking.
277
+ *
278
+ * Returns `undefined` for generic / ambiguous labels (login buttons,
279
+ * close, OK, etc.) where a substring text match could trivially fire on
280
+ * the wrong element.
281
+ */
282
+ private extractClickKeyword;
262
283
  }
263
284
 
264
285
  /**
@@ -305,6 +326,20 @@ declare class BrowserProvider {
305
326
  extractDOM(opts?: {
306
327
  maxElements?: number;
307
328
  }): Promise<string>;
329
+ /**
330
+ * Deterministic, DOM-based click using Playwright's text locator.
331
+ *
332
+ * Returns `true` if a matching, visible, clickable element was found and
333
+ * clicked within `timeout` ms; `false` otherwise (so the caller can fall
334
+ * back to coordinate clicking). Substring-matches by default — e.g.
335
+ * `clickByText("Cheapest")` matches "Cheapest · 23-28 days · $2,550".
336
+ *
337
+ * This is the same pattern browser-use uses in DOM-only mode and is far
338
+ * more reliable than coordinate clicks for text-bearing targets.
339
+ */
340
+ clickByText(keyword: string, opts?: {
341
+ timeout?: number;
342
+ }): Promise<boolean>;
308
343
  getPageInfo(): Promise<PageInfo>;
309
344
  waitForStable(minWait?: number): Promise<void>;
310
345
  newTab(url?: string): Promise<string>;
package/dist/index.js CHANGED
@@ -112,7 +112,11 @@ function buildStealthContextOpts(config, viewport) {
112
112
  locale: config.locale ?? "en-US",
113
113
  timezoneId: config.timezone ?? "America/New_York",
114
114
  colorScheme: "light",
115
- deviceScaleFactor: 2,
115
+ // Default to 1 — matches the most common real-user setup and avoids
116
+ // visual zoom/stretch in headed mode on non-Retina displays (the host
117
+ // OS would downsample a 2× rendered surface). Users on Retina-only
118
+ // deployments can opt back into DPR=2 for sharper screenshots.
119
+ deviceScaleFactor: config.deviceScaleFactor ?? 1,
116
120
  hasTouch: false,
117
121
  javaScriptEnabled: true,
118
122
  ignoreHTTPSErrors: config.ignoreHTTPSErrors ?? false
@@ -304,7 +308,24 @@ var BrowserProvider = class {
304
308
  "[contenteditable='true']",
305
309
  "[tabindex]:not([tabindex='-1'])"
306
310
  ];
307
- const all = Array.from(doc.querySelectorAll(selectors.join(",")));
311
+ const semantic = Array.from(doc.querySelectorAll(selectors.join(",")));
312
+ const POINTER_SCAN_CAP = 2e3;
313
+ const pointerCandidates = [];
314
+ const allEls = doc.querySelectorAll("*");
315
+ for (let i = 0; i < allEls.length && pointerCandidates.length < POINTER_SCAN_CAP; i++) {
316
+ const el = allEls[i];
317
+ if (!el) continue;
318
+ const tag = el.tagName.toLowerCase();
319
+ if (tag === "html" || tag === "body" || tag === "head" || tag === "script" || tag === "style") continue;
320
+ const rect = el.getBoundingClientRect();
321
+ if (rect.width < 4 || rect.height < 4) continue;
322
+ if (rect.bottom < 0 || rect.top > win.innerHeight) continue;
323
+ if (rect.right < 0 || rect.left > win.innerWidth) continue;
324
+ const style = win.getComputedStyle(el);
325
+ if (style.cursor !== "pointer") continue;
326
+ pointerCandidates.push(el);
327
+ }
328
+ const all = semantic.concat(pointerCandidates);
308
329
  const vh = win.innerHeight;
309
330
  const vw = win.innerWidth;
310
331
  const entries = [];
@@ -326,6 +347,7 @@ var BrowserProvider = class {
326
347
  if (hit && hit !== el && !el.contains(hit) && !hit.contains(el)) {
327
348
  continue;
328
349
  }
350
+ if (hit && el.contains(hit)) seen.add(hit);
329
351
  } catch {
330
352
  }
331
353
  seen.add(el);
@@ -354,6 +376,29 @@ var BrowserProvider = class {
354
376
  }, max);
355
377
  return elements;
356
378
  }
379
+ /**
380
+ * Deterministic, DOM-based click using Playwright's text locator.
381
+ *
382
+ * Returns `true` if a matching, visible, clickable element was found and
383
+ * clicked within `timeout` ms; `false` otherwise (so the caller can fall
384
+ * back to coordinate clicking). Substring-matches by default — e.g.
385
+ * `clickByText("Cheapest")` matches "Cheapest · 23-28 days · $2,550".
386
+ *
387
+ * This is the same pattern browser-use uses in DOM-only mode and is far
388
+ * more reliable than coordinate clicks for text-bearing targets.
389
+ */
390
+ async clickByText(keyword, opts) {
391
+ this.ensurePage();
392
+ const timeout = opts?.timeout ?? 3e3;
393
+ try {
394
+ const locator = this.page.locator(`text=${keyword}`).first();
395
+ await locator.click({ timeout });
396
+ await this.humanPause();
397
+ return true;
398
+ } catch {
399
+ return false;
400
+ }
401
+ }
357
402
  // ── Page Info ────────────────────────────────────────────────────────
358
403
  async getPageInfo() {
359
404
  this.ensurePage();
@@ -526,6 +571,7 @@ function buildSystemPrompt(viewport, extraInstructions, credentialKeys) {
526
571
  `### click`,
527
572
  `Click at a specific coordinate. Use for buttons, links, inputs, checkboxes, etc.`,
528
573
  `\`{ "action": "click", "x": <number>, "y": <number>, "description": "<what you are clicking>" }\``,
574
+ `IMPORTANT: when the target has visible text, ALWAYS put that text in single or double quotes inside the description, e.g. \`"description": "Click on 'Cheapest' tab"\` or \`"description": "Press the \\"Sign in\\" button"\`. The runtime uses the quoted phrase to do a deterministic DOM text-locator click and only falls back to (x,y) if no match is found, which dramatically improves accuracy on dynamic pages.`,
529
575
  ``,
530
576
  `### type`,
531
577
  `Type text. If x/y are provided, click that position first (to focus the input), then type. If omitted, types into the currently focused element. To press Enter after typing (e.g., to submit a search), append "\\n" to the text.`,
@@ -930,9 +976,20 @@ ${memoryContext}` : memoryContext;
930
976
  async executeAction(browser, action) {
931
977
  try {
932
978
  switch (action.action) {
933
- case "click":
934
- await browser.click(action.x, action.y);
979
+ case "click": {
980
+ const keyword = this.extractClickKeyword(action.description);
981
+ let clicked = false;
982
+ if (keyword) {
983
+ clicked = await browser.clickByText(keyword);
984
+ if (clicked) {
985
+ this.logger.debug("Clicked by text", { keyword });
986
+ }
987
+ }
988
+ if (!clicked) {
989
+ await browser.click(action.x, action.y);
990
+ }
935
991
  break;
992
+ }
936
993
  case "type": {
937
994
  const resolvedText = this.credentials ? this.credentials.resolve(action.text) : action.text;
938
995
  if (action.x != null && action.y != null) {
@@ -969,6 +1026,52 @@ ${memoryContext}` : memoryContext;
969
1026
  sleep(ms) {
970
1027
  return new Promise((resolve) => setTimeout(resolve, ms));
971
1028
  }
1029
+ /**
1030
+ * Parse a quoted target keyword from a click action's `description`.
1031
+ *
1032
+ * The system prompt asks the model to include the visible label of its
1033
+ * click target in quotes (e.g. `"Click on 'Cheapest' tab"`). When a usable
1034
+ * quoted phrase is present we return it so `executeAction` can try a
1035
+ * deterministic Playwright text locator before falling back to
1036
+ * coordinate clicking.
1037
+ *
1038
+ * Returns `undefined` for generic / ambiguous labels (login buttons,
1039
+ * close, OK, etc.) where a substring text match could trivially fire on
1040
+ * the wrong element.
1041
+ */
1042
+ extractClickKeyword(description) {
1043
+ if (!description) return void 0;
1044
+ const match = description.match(/['"\u2018\u2019\u201C\u201D]([^'"\u2018\u2019\u201C\u201D]{1,80})['"\u2018\u2019\u201C\u201D]/);
1045
+ if (!match) return void 0;
1046
+ const keyword = match[1].trim();
1047
+ if (!keyword || keyword.length < 2) return void 0;
1048
+ const skip = /* @__PURE__ */ new Set([
1049
+ "log in",
1050
+ "login",
1051
+ "sign in",
1052
+ "sign up",
1053
+ "submit",
1054
+ "close",
1055
+ "ok",
1056
+ "okay",
1057
+ "cancel",
1058
+ "yes",
1059
+ "no",
1060
+ "x",
1061
+ "continue",
1062
+ "next",
1063
+ "back",
1064
+ "accept",
1065
+ "dismiss",
1066
+ "got it",
1067
+ "agree",
1068
+ "i agree",
1069
+ "allow",
1070
+ "deny"
1071
+ ]);
1072
+ if (skip.has(keyword.toLowerCase())) return void 0;
1073
+ return keyword;
1074
+ }
972
1075
  };
973
1076
 
974
1077
  // src/credential-vault.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentium/browser",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "Browser automation agent for Agentium using Playwright",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,7 +46,7 @@
46
46
  "typescript": "^5.6.0"
47
47
  },
48
48
  "peerDependencies": {
49
- "@agentium/core": "^2.0.7",
49
+ "@agentium/core": "^2.0.8",
50
50
  "playwright": ">=1.40.0"
51
51
  },
52
52
  "peerDependenciesMeta": {