@browserless/agent 14.0.3 → 14.0.5

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@browserless/agent",
3
3
  "description": "Drive a Puppeteer page toward a natural-language goal using finite decisions.",
4
4
  "homepage": "https://browserless.js.org",
5
- "version": "14.0.3",
5
+ "version": "14.0.5",
6
6
  "main": "src",
7
7
  "author": {
8
8
  "email": "hello@microlink.io",
@@ -59,5 +59,5 @@
59
59
  "access": "public"
60
60
  },
61
61
  "types": "src/index.d.ts",
62
- "gitHead": "f26f8ff96f57c2f2a848837097b560bd25ddd78a"
62
+ "gitHead": "6e8791c6a9061de43b9fc9e9b8e483bcfb970ef2"
63
63
  }
package/src/model.js CHANGED
@@ -94,7 +94,7 @@ const actionSpace = actions => {
94
94
  indices.set(action.node, index)
95
95
  elements.push({
96
96
  index,
97
- label: action.label.split(' → ')[0],
97
+ label: action.kind === 'select' ? action.label.split(' → ')[0] : action.label,
98
98
  role: action.role,
99
99
  value: action.current_value ?? action.value,
100
100
  checked: action.checked,
@@ -345,7 +345,14 @@ const fieldText = async (goal, action, state, history, model, options) => {
345
345
  system: TEXT_VALUE,
346
346
  prompt: JSON.stringify({
347
347
  goal,
348
- field: { label: action.label, role: action.role, value: action.value },
348
+ field: {
349
+ id: action.id,
350
+ node: action.node,
351
+ label: action.label,
352
+ role: action.role,
353
+ value: action.value,
354
+ context: state.scopes?.[action.node] ?? ''
355
+ },
349
356
  page: { title: state.title, text: state.text },
350
357
  recent_actions: history.slice(-TEXT_HISTORY_LENGTH)
351
358
  }),
package/src/snapshot.js CHANGED
@@ -1,4 +1,4 @@
1
- /* global HTMLElement, location, scrollX, scrollY, innerWidth, innerHeight */
1
+ /* global HTMLElement, getComputedStyle, location, scrollX, scrollY, innerWidth, innerHeight */
2
2
  // Adapted from browser-use/jev-ultrafast snapshot.js (MIT). See README.md.
3
3
  module.exports = function snapshot () {
4
4
  return (() => {
@@ -102,7 +102,7 @@ module.exports = function snapshot () {
102
102
  .map(l => name(l, seen))
103
103
  .filter(Boolean)
104
104
  .join(' ') ||
105
- (['button', 'submit', 'reset'].includes(e.type) ? e.value : '') ||
105
+ (e.tagName === 'INPUT' && ['button', 'submit', 'reset'].includes(e.type) ? e.value : '') ||
106
106
  e.getAttribute('alt') ||
107
107
  (e.tagName === 'INPUT'
108
108
  ? ''
@@ -283,6 +283,74 @@ module.exports = function snapshot () {
283
283
  }
284
284
  }
285
285
  }
286
+ const collected = new Set(actions.map(action => cache.nodes.get(action.node)))
287
+ const listSelector = 'ul,ol,[role="list"],[role="listbox"],[role="menu"],[role="tablist"]'
288
+ const pointer = element => {
289
+ if (getComputedStyle(element).cursor !== 'pointer') return false
290
+ const parent = flatParent(element)
291
+ if (!parent || getComputedStyle(parent).cursor !== 'pointer') return true
292
+ return parent.matches(listSelector)
293
+ }
294
+ const inLayer = element => {
295
+ for (let current = element; current; current = flatParent(current)) {
296
+ if (current === document.body || current === document.documentElement) return false
297
+ const position = getComputedStyle(current).position
298
+ if (position === 'absolute' || position === 'fixed' || position === 'sticky') return true
299
+ }
300
+ return false
301
+ }
302
+ const pointerLists = new WeakMap()
303
+ const inPointerList = element => {
304
+ const parent = flatParent(element)
305
+ if (!parent?.matches(listSelector)) return false
306
+ let eligible = pointerLists.get(parent)
307
+ if (eligible == null) {
308
+ eligible = [...parent.children].filter(pointer).length >= 2
309
+ pointerLists.set(parent, eligible)
310
+ }
311
+ return eligible
312
+ }
313
+ const candidates = []
314
+ for (const element of queryAll('li,div,span,td,dd,p')) {
315
+ if (collected.has(element) || !visible(element) || !pointer(element)) continue
316
+ if (element.matches(':disabled') || cache.closest(element, '[aria-disabled="true"]')) continue
317
+ if (element.querySelector(selector)) continue
318
+ const rect = element.getBoundingClientRect()
319
+ const x = rect.x + rect.width / 2
320
+ const y = rect.y + rect.height / 2
321
+ if (
322
+ rect.width <= 0 ||
323
+ rect.height <= 0 ||
324
+ x < 0 ||
325
+ y < 0 ||
326
+ x >= innerWidth ||
327
+ y >= innerHeight
328
+ ) {
329
+ continue
330
+ }
331
+ const hit = cache.elementFromPoint(x, y)
332
+ if (hit && !cache.contains(element, hit)) continue
333
+ if (!inLayer(element) && !inPointerList(element)) continue
334
+ const label = (name(element) || '').replace(/\s+/g, ' ').trim().slice(0, 120)
335
+ if (!label) continue
336
+ candidates.push({ element, rect, label })
337
+ }
338
+ const rows = candidates.filter(
339
+ ({ element }) =>
340
+ !candidates.some(
341
+ other => other.element !== element && cache.contains(element, other.element)
342
+ )
343
+ )
344
+ for (const { element, rect, label } of rows.slice(0, 40)) {
345
+ actions.push({
346
+ node: identity(element),
347
+ role: 'button',
348
+ label,
349
+ rect: { x: rect.x, y: rect.y, w: rect.width, h: rect.height },
350
+ kind: 'click',
351
+ value: ''
352
+ })
353
+ }
286
354
  const words = []
287
355
  const range = document.createRange()
288
356
  let length = 0
@@ -354,8 +422,18 @@ module.exports = function snapshot () {
354
422
  const height = document.documentElement.scrollHeight
355
423
  const pageKey = cache.pageKey()
356
424
  const guards = {}
425
+ const scopes = {}
357
426
  for (const a of actions) {
358
- if (!(a.node in guards)) guards[a.node] = cache.guard(cache.nodes.get(a.node))
427
+ if (a.node in guards) continue
428
+ const element = cache.nodes.get(a.node)
429
+ guards[a.node] = cache.guard(element)
430
+ const scope =
431
+ cache.closest(element, 'form,dialog,[role="dialog"],article,li,tr,[role="row"]') ||
432
+ flatParent(element)
433
+ scopes[a.node] =
434
+ (scope instanceof HTMLElement
435
+ ? readInnerText.call(scope).slice(0, 1000)
436
+ : scope?.textContent?.slice(0, 1000)) || ''
359
437
  }
360
438
  // Compare meaning and identity. Geometry is always resolved and hit-tested just before input.
361
439
  const semantics = actions.map(({ rect, ...action }) => action)
@@ -395,6 +473,7 @@ module.exports = function snapshot () {
395
473
  marker,
396
474
  pageKey,
397
475
  guards,
476
+ scopes,
398
477
  omittedActions
399
478
  }
400
479
  })()