@agentium/browser 2.0.6 → 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
@@ -206,14 +210,10 @@ var BrowserProvider = class {
206
210
  const launchOpts = {
207
211
  headless: opts?.headless ?? true
208
212
  };
209
- const windowSizeArg = `--window-size=${this._viewport.width},${this._viewport.height}`;
210
- const windowPositionArg = "--window-position=0,0";
211
213
  if (stealthEnabled) {
212
214
  const { args, proxy } = buildStealthLaunchArgs(stealthCfg);
213
- launchOpts.args = [...args, windowSizeArg, windowPositionArg];
215
+ launchOpts.args = args;
214
216
  if (proxy) launchOpts.proxy = proxy;
215
- } else {
216
- launchOpts.args = [windowSizeArg, windowPositionArg];
217
217
  }
218
218
  this.browser = await chromium.launch(launchOpts);
219
219
  let contextOpts;
@@ -346,7 +346,24 @@ var BrowserProvider = class {
346
346
  "[contenteditable='true']",
347
347
  "[tabindex]:not([tabindex='-1'])"
348
348
  ];
349
- 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);
350
367
  const vh = win.innerHeight;
351
368
  const vw = win.innerWidth;
352
369
  const entries = [];
@@ -368,6 +385,7 @@ var BrowserProvider = class {
368
385
  if (hit && hit !== el && !el.contains(hit) && !hit.contains(el)) {
369
386
  continue;
370
387
  }
388
+ if (hit && el.contains(hit)) seen.add(hit);
371
389
  } catch {
372
390
  }
373
391
  seen.add(el);
@@ -396,6 +414,29 @@ var BrowserProvider = class {
396
414
  }, max);
397
415
  return elements;
398
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
+ }
399
440
  // ── Page Info ────────────────────────────────────────────────────────
400
441
  async getPageInfo() {
401
442
  this.ensurePage();
@@ -568,6 +609,7 @@ function buildSystemPrompt(viewport, extraInstructions, credentialKeys) {
568
609
  `### click`,
569
610
  `Click at a specific coordinate. Use for buttons, links, inputs, checkboxes, etc.`,
570
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.`,
571
613
  ``,
572
614
  `### type`,
573
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.`,
@@ -972,9 +1014,20 @@ ${memoryContext}` : memoryContext;
972
1014
  async executeAction(browser, action) {
973
1015
  try {
974
1016
  switch (action.action) {
975
- case "click":
976
- 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
+ }
977
1029
  break;
1030
+ }
978
1031
  case "type": {
979
1032
  const resolvedText = this.credentials ? this.credentials.resolve(action.text) : action.text;
980
1033
  if (action.x != null && action.y != null) {
@@ -1011,6 +1064,52 @@ ${memoryContext}` : memoryContext;
1011
1064
  sleep(ms) {
1012
1065
  return new Promise((resolve) => setTimeout(resolve, ms));
1013
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
+ }
1014
1113
  };
1015
1114
 
1016
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
@@ -168,14 +172,10 @@ var BrowserProvider = class {
168
172
  const launchOpts = {
169
173
  headless: opts?.headless ?? true
170
174
  };
171
- const windowSizeArg = `--window-size=${this._viewport.width},${this._viewport.height}`;
172
- const windowPositionArg = "--window-position=0,0";
173
175
  if (stealthEnabled) {
174
176
  const { args, proxy } = buildStealthLaunchArgs(stealthCfg);
175
- launchOpts.args = [...args, windowSizeArg, windowPositionArg];
177
+ launchOpts.args = args;
176
178
  if (proxy) launchOpts.proxy = proxy;
177
- } else {
178
- launchOpts.args = [windowSizeArg, windowPositionArg];
179
179
  }
180
180
  this.browser = await chromium.launch(launchOpts);
181
181
  let contextOpts;
@@ -308,7 +308,24 @@ var BrowserProvider = class {
308
308
  "[contenteditable='true']",
309
309
  "[tabindex]:not([tabindex='-1'])"
310
310
  ];
311
- 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);
312
329
  const vh = win.innerHeight;
313
330
  const vw = win.innerWidth;
314
331
  const entries = [];
@@ -330,6 +347,7 @@ var BrowserProvider = class {
330
347
  if (hit && hit !== el && !el.contains(hit) && !hit.contains(el)) {
331
348
  continue;
332
349
  }
350
+ if (hit && el.contains(hit)) seen.add(hit);
333
351
  } catch {
334
352
  }
335
353
  seen.add(el);
@@ -358,6 +376,29 @@ var BrowserProvider = class {
358
376
  }, max);
359
377
  return elements;
360
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
+ }
361
402
  // ── Page Info ────────────────────────────────────────────────────────
362
403
  async getPageInfo() {
363
404
  this.ensurePage();
@@ -530,6 +571,7 @@ function buildSystemPrompt(viewport, extraInstructions, credentialKeys) {
530
571
  `### click`,
531
572
  `Click at a specific coordinate. Use for buttons, links, inputs, checkboxes, etc.`,
532
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.`,
533
575
  ``,
534
576
  `### type`,
535
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.`,
@@ -934,9 +976,20 @@ ${memoryContext}` : memoryContext;
934
976
  async executeAction(browser, action) {
935
977
  try {
936
978
  switch (action.action) {
937
- case "click":
938
- 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
+ }
939
991
  break;
992
+ }
940
993
  case "type": {
941
994
  const resolvedText = this.credentials ? this.credentials.resolve(action.text) : action.text;
942
995
  if (action.x != null && action.y != null) {
@@ -973,6 +1026,52 @@ ${memoryContext}` : memoryContext;
973
1026
  sleep(ms) {
974
1027
  return new Promise((resolve) => setTimeout(resolve, ms));
975
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
+ }
976
1075
  };
977
1076
 
978
1077
  // src/credential-vault.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentium/browser",
3
- "version": "2.0.6",
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.6",
49
+ "@agentium/core": "^2.0.8",
50
50
  "playwright": ">=1.40.0"
51
51
  },
52
52
  "peerDependenciesMeta": {