@mobileaidev/ai-app-bridge-web 0.4.6 → 0.4.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/README.md CHANGED
@@ -23,6 +23,14 @@ unchecked. Use the current SDK with the current Host; missing `checked` is an
23
23
  invalid snapshot. A semantic click must still be followed by a fresh observation
24
24
  and an independent business check where persistence matters.
25
25
 
26
+ Visible controls without text, an ARIA label or a placeholder may also expose
27
+ `context.ancestors`: up to two distinct text samples from their actual DOM
28
+ ancestors, within six parent levels and 160 characters per sample. Collection
29
+ is bounded to 64 nodes per ancestor, excludes form values and hidden branches,
30
+ and stops before BODY/HTML. Truncation flags describe the limits. This fresh
31
+ observation helps an agent distinguish otherwise unlabelled record menus; it
32
+ does not infer a business row or change selectors and action identity.
33
+
26
34
  ## Capture windows and network bodies
27
35
 
28
36
  Network body capture is explicitly enabled with `captureRequestBodies: true`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mobileaidev/ai-app-bridge-web",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "Browser SDK for AI App Bridge Web sessions.",
5
5
  "main": "src/index.js",
6
6
  "browser": "src/index.js",
package/src/index.js CHANGED
@@ -413,6 +413,56 @@
413
413
  role: attr(hit, 'role'), className: typeof hit.className === 'string' ? trimText(hit.className, 300) : '' } : null };
414
414
  }
415
415
 
416
+ function ancestorContext(element, identity, cache) {
417
+ if ([identity.text, identity.ariaLabel, identity.placeholder].some(value => value.trim())) return null;
418
+ const ancestors = [], seen = new Set();
419
+ let parent = element.parentElement, depth = 1;
420
+ while (parent && !['BODY', 'HTML'].includes(parent.tagName) && depth <= 6 && ancestors.length < 2) {
421
+ const sample = ancestorText(parent, cache);
422
+ if (sample.text && !seen.has(sample.text)) {
423
+ seen.add(sample.text);
424
+ const id = text(parent.id), role = attr(parent, 'role');
425
+ ancestors.push({ depth, tag: text(parent.tagName).toLowerCase(), id: id.slice(0, 80), role: role.slice(0, 40),
426
+ text: sample.text, textTruncated: sample.truncated, identityTruncated: id.length > 80 || role.length > 40 });
427
+ }
428
+ parent = parent.parentElement; depth++;
429
+ }
430
+ // DOM context describes actual ancestors, not a guessed business row or a visibility guarantee.
431
+ return ancestors.length ? { source: 'ancestor-dom-text', ancestors,
432
+ truncated: ancestors.some(item => item.textTruncated || item.identityTruncated)
433
+ || Boolean(parent && !['BODY', 'HTML'].includes(parent.tagName)) } : null;
434
+ }
435
+
436
+ function ancestorText(root, cache) {
437
+ if (cache.has(root)) return cache.get(root);
438
+ let node = root, visited = 0, value = '', truncated = false;
439
+ while (node && visited < 64) {
440
+ visited++;
441
+ let descend = true;
442
+ if (node.nodeType === 1) {
443
+ const style = node.ownerDocument?.defaultView?.getComputedStyle(node);
444
+ descend = !['SCRIPT', 'STYLE', 'TEMPLATE', 'NOSCRIPT', 'INPUT', 'TEXTAREA', 'SELECT'].includes(node.tagName)
445
+ && !isSensitiveInput(node) && !node.hidden && attr(node, 'aria-hidden') !== 'true'
446
+ && style?.display !== 'none' && !['hidden', 'collapse'].includes(style?.visibility) && style?.opacity !== '0';
447
+ } else if (node.nodeType === 3) {
448
+ const raw = text(node.nodeValue);
449
+ // Normalize a bounded raw sample so indentation does not consume the readable-text budget.
450
+ const part = raw.slice(0, 1024).replace(/\s+/g, ' ').trim();
451
+ const next = value + (value && part ? ' ' : '') + part;
452
+ value = next.slice(0, 160);
453
+ if (raw.length > 1024 || next.length > 160) { truncated = true; break; }
454
+ }
455
+ if (descend && node.firstChild) node = node.firstChild;
456
+ else {
457
+ while (node !== root && !node.nextSibling) node = node.parentNode;
458
+ node = node === root ? null : node.nextSibling;
459
+ }
460
+ }
461
+ const result = { text: value, truncated: truncated || Boolean(node) };
462
+ cache.set(root, result);
463
+ return result;
464
+ }
465
+
416
466
  function snapshotDom(options = {}) {
417
467
  const doc = options.document || globalValue('document');
418
468
  if (!doc || !doc.querySelectorAll) {
@@ -427,18 +477,22 @@
427
477
  const maxControls = Number(options.maxControls || 200);
428
478
  const offset = options.offset ?? 0;
429
479
  const controls = [];
480
+ const contextCache = new WeakMap();
430
481
  const matched = options.query ? matchingElements(options.query, doc) : doc.querySelectorAll(selector);
431
482
  const elements = Array.prototype.slice.call(matched, offset, offset + maxControls);
432
483
  elements.forEach((element, index) => {
433
484
  const sensitive = isSensitiveInput(element);
434
485
  const elementValue = text(element.value);
486
+ const identity = elementIdentity(element, doc), visible = elementVisible(element);
487
+ const context = visible ? ancestorContext(element, identity, contextCache) : null;
435
488
  controls.push({
436
- ...elementIdentity(element, doc),
489
+ ...identity,
490
+ ...(context ? { context } : {}),
437
491
  index: offset + index,
438
492
  sensitive,
439
493
  ...(sensitive ? { valueLength: elementValue.length } : {}),
440
494
  disabled: elementDisabled(element),
441
- visible: elementVisible(element),
495
+ visible,
442
496
  editable: elementEditable(element),
443
497
  checked: elementChecked(element),
444
498
  ariaChecked: element.getAttribute('aria-checked'),
@@ -493,12 +547,31 @@
493
547
  element.dispatchEvent(new win.InputEvent('input', { bubbles: true, inputType: 'insertReplacementText', data: value }));
494
548
  ensureTarget(selected, task, { focus: true, edited: true }); task.permit(); element.dispatchEvent(new win.Event('change', { bubbles: true }));
495
549
  }
496
- ensureTarget(selected, task, { edited: true });
550
+ // The final input/change dispatch may collapse, disable or remove its own
551
+ // editor. Report what happened to that original object; never reselect it
552
+ // or treat its resulting layout as a precondition for the completed input.
553
+ const targetState = { samePage: samePage(task.pageRef(), selected.ref.pageRef),
554
+ sameDocument: element.ownerDocument === globalValue('document'), connected: element.isConnected,
555
+ visible: elementVisible(element), disabled: elementDisabled(element), editable: elementEditable(element),
556
+ focused: doc.activeElement === element, bounds: bounds(element) };
557
+ if (!targetState.samePage || !targetState.sameDocument || !targetState.connected) {
558
+ return { ok: false, error: targetState.samePage ? 'web_element_changed' : 'reobserve_required',
559
+ resolved: selected.ref, inputDispatched: true, valueState: 'unconfirmed', targetState,
560
+ message: 'Input was dispatched, but its original element is no longer in the current page. Reobserve before continuing.' };
561
+ }
562
+ recordTargetChanges(selected, elementIdentity(element), true);
563
+ const warnings = targetWarnings(selected).warnings ?? [];
564
+ if (!targetState.visible || targetState.disabled || !targetState.editable || !targetState.focused)
565
+ warnings.push({ code: 'target_state_changed_after_input',
566
+ message: 'Input was dispatched; the original element state changed afterwards. Inspect targetState before another action.' });
497
567
  const sensitive = isSensitiveInput(element);
498
568
  return {
499
569
  ok: true,
500
570
  resolved: selected.ref,
501
- ...targetWarnings(selected),
571
+ inputDispatched: true,
572
+ valueState: 'observed',
573
+ targetState,
574
+ ...(warnings.length ? { warnings } : {}),
502
575
  sensitive,
503
576
  valueLength: (richText ? element.innerText : element.value).length,
504
577
  ...(sensitive ? {} : { value: richText ? element.innerText : element.value }),