mobile-debug-mcp 0.26.2 → 0.26.4
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/AGENTS.md +3 -0
- package/dist/interact/index.js +600 -70
- package/dist/observe/ios.js +1 -1
- package/dist/server/tool-definitions.js +59 -1
- package/dist/server/tool-handlers.js +25 -0
- package/dist/server-core.js +1 -1
- package/dist/utils/android/utils.js +2 -2
- package/docs/CHANGELOG.md +6 -0
- package/docs/ROADMAP.md +72 -16
- package/docs/rfcs/007-actionability-resolution-and-executable-target-selection.md +277 -0
- package/docs/rfcs/008-adjustable-control-support-and-semantic-value-manipulation.md +273 -0
- package/docs/specs/mcp-tooling-spec-v1.md +1 -1
- package/docs/tools/interact.md +30 -1
- package/package.json +1 -1
- package/src/interact/index.ts +761 -72
- package/src/observe/ios.ts +1 -1
- package/src/server/tool-definitions.ts +59 -1
- package/src/server/tool-handlers.ts +26 -0
- package/src/server-core.ts +1 -1
- package/src/types.ts +90 -0
- package/src/utils/android/utils.ts +2 -2
- package/test/unit/interact/adjust_control.test.ts +365 -0
- package/test/unit/observe/find_element.test.ts +5 -0
- package/test/unit/observe/state_extraction.test.ts +24 -0
- package/test/unit/server/contract.test.ts +8 -0
- package/test/unit/server/response_shapes.test.ts +39 -0
package/dist/interact/index.js
CHANGED
|
@@ -203,6 +203,96 @@ export class ToolsInteract {
|
|
|
203
203
|
semantic: element.semantic ?? null
|
|
204
204
|
};
|
|
205
205
|
}
|
|
206
|
+
static _summarizeResolutionCandidate(candidate) {
|
|
207
|
+
const bounds = ToolsInteract._normalizeBounds(candidate.el.bounds);
|
|
208
|
+
return {
|
|
209
|
+
text: candidate.el.text ?? null,
|
|
210
|
+
resource_id: candidate.el.resourceId ?? candidate.el.resourceID ?? candidate.el.id ?? null,
|
|
211
|
+
accessibility_id: candidate.el.contentDescription ?? candidate.el.contentDesc ?? candidate.el.accessibilityLabel ?? candidate.el.label ?? null,
|
|
212
|
+
class: candidate.el.type ?? candidate.el.class ?? null,
|
|
213
|
+
bounds: bounds
|
|
214
|
+
? { left: bounds[0], top: bounds[1], right: bounds[2], bottom: bounds[3] }
|
|
215
|
+
: null,
|
|
216
|
+
clickable: !!candidate.el.clickable,
|
|
217
|
+
enabled: !!candidate.el.enabled,
|
|
218
|
+
score: candidate.score,
|
|
219
|
+
reason: candidate.reason
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
static _isAdjustableControl(el) {
|
|
223
|
+
if (!el)
|
|
224
|
+
return false;
|
|
225
|
+
const type = ToolsInteract._normalize(el.type ?? el.class ?? '');
|
|
226
|
+
const role = ToolsInteract._normalize(el.role ?? '');
|
|
227
|
+
return !!el.state?.value_range || /slider|seekbar|stepper|adjustable|range/.test(type) || /slider|seekbar|stepper|adjustable|range/.test(role);
|
|
228
|
+
}
|
|
229
|
+
static _readNumericControlValue(el, property) {
|
|
230
|
+
if (!el?.state)
|
|
231
|
+
return null;
|
|
232
|
+
const stateValue = el.state[property];
|
|
233
|
+
if (typeof stateValue === 'number' && Number.isFinite(stateValue))
|
|
234
|
+
return stateValue;
|
|
235
|
+
if (property === 'value' || property === 'raw_value') {
|
|
236
|
+
const fallback = el.state.raw_value ?? el.state.value;
|
|
237
|
+
if (typeof fallback === 'number' && Number.isFinite(fallback))
|
|
238
|
+
return fallback;
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
static _buildControlPoint(bounds, ratio, axis) {
|
|
243
|
+
const clampedRatio = Math.max(0, Math.min(1, ratio));
|
|
244
|
+
const [left, top, right, bottom] = bounds;
|
|
245
|
+
const width = Math.max(1, right - left);
|
|
246
|
+
const height = Math.max(1, bottom - top);
|
|
247
|
+
const insetX = Math.max(8, Math.floor(width * 0.08));
|
|
248
|
+
const insetY = Math.max(8, Math.floor(height * 0.08));
|
|
249
|
+
if (axis === 'vertical') {
|
|
250
|
+
const usableHeight = Math.max(1, height - (insetY * 2));
|
|
251
|
+
return {
|
|
252
|
+
x: Math.floor((left + right) / 2),
|
|
253
|
+
y: Math.floor(bottom - insetY - (usableHeight * clampedRatio))
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const usableWidth = Math.max(1, width - (insetX * 2));
|
|
257
|
+
return {
|
|
258
|
+
x: Math.floor(left + insetX + (usableWidth * clampedRatio)),
|
|
259
|
+
y: Math.floor((top + bottom) / 2)
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
static _buildConservativeControlPoint(bounds, targetValue, currentValue, min, max, axis) {
|
|
263
|
+
const range = Math.max(1, max - min);
|
|
264
|
+
const targetRatio = (targetValue - min) / range;
|
|
265
|
+
const stepRatio = 1 / range;
|
|
266
|
+
const centerBias = stepRatio / 2;
|
|
267
|
+
const direction = currentValue === null ? 0 : Math.sign(targetValue - currentValue);
|
|
268
|
+
const controlLengthPx = axis === 'vertical' ? Math.max(1, bounds[3] - bounds[1]) : Math.max(1, bounds[2] - bounds[0]);
|
|
269
|
+
const edgeWindow = Math.max(3, Math.floor(range * 0.1));
|
|
270
|
+
const isNearLowEdge = targetValue - min <= edgeWindow;
|
|
271
|
+
const isNearHighEdge = max - targetValue <= edgeWindow;
|
|
272
|
+
const directionBias = direction > 0
|
|
273
|
+
? -stepRatio * 0.15
|
|
274
|
+
: direction < 0
|
|
275
|
+
? stepRatio * 0.65
|
|
276
|
+
: 0;
|
|
277
|
+
const pixelBasedMargin = Math.min(0.03, Math.max(0.005, 2 / controlLengthPx));
|
|
278
|
+
const endpointMargin = Math.max(stepRatio * 0.5, pixelBasedMargin);
|
|
279
|
+
const edgeBias = isNearLowEdge
|
|
280
|
+
? endpointMargin
|
|
281
|
+
: isNearHighEdge
|
|
282
|
+
? Math.max(stepRatio * 0.4, endpointMargin * 0.75)
|
|
283
|
+
: 0;
|
|
284
|
+
const safeRatio = Math.min(1 - (endpointMargin * 0.25), Math.max(endpointMargin, targetRatio + centerBias + directionBias + edgeBias));
|
|
285
|
+
return ToolsInteract._buildControlPoint(bounds, safeRatio, axis);
|
|
286
|
+
}
|
|
287
|
+
static _controlAxis(el, bounds) {
|
|
288
|
+
const type = ToolsInteract._normalize(el.type ?? el.class ?? '');
|
|
289
|
+
const role = ToolsInteract._normalize(el.role ?? '');
|
|
290
|
+
if (/vertical/.test(type) || /vertical/.test(role))
|
|
291
|
+
return 'vertical';
|
|
292
|
+
if (/horizontal/.test(type) || /horizontal/.test(role))
|
|
293
|
+
return 'horizontal';
|
|
294
|
+
return (bounds[3] - bounds[1]) > (bounds[2] - bounds[0]) ? 'vertical' : 'horizontal';
|
|
295
|
+
}
|
|
206
296
|
static _actionFailure(actionType, selector, resolved, failureCode, retryable, uiFingerprintBefore, uiFingerprintAfter, sourceModule = 'interact') {
|
|
207
297
|
return buildActionExecutionResult({
|
|
208
298
|
actionType,
|
|
@@ -394,6 +484,388 @@ export class ToolsInteract {
|
|
|
394
484
|
sourceModule: 'interact'
|
|
395
485
|
});
|
|
396
486
|
}
|
|
487
|
+
static async adjustControlHandler({ selector, element_id, property = 'value', targetValue, tolerance = 0, maxAttempts = 3, platform, deviceId }) {
|
|
488
|
+
const actionType = 'adjust_control';
|
|
489
|
+
const targetSelector = selector ?? (element_id ? { elementId: element_id } : null);
|
|
490
|
+
const normalizedTolerance = Number.isFinite(tolerance) ? Math.max(0, tolerance) : 0;
|
|
491
|
+
const attemptsLimit = Math.max(1, Math.floor(Number(maxAttempts) || 1));
|
|
492
|
+
const sourcePlatform = platform || 'android';
|
|
493
|
+
let resolvedPlatform = sourcePlatform;
|
|
494
|
+
let resolvedDeviceId = deviceId;
|
|
495
|
+
const fingerprintBefore = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
496
|
+
let semanticFallbackElement = null;
|
|
497
|
+
const buildFailure = (failureCode, reason, resolved, device, actualState, attempts, adjustmentMode = 'gesture', retryable = false, uiFingerprintAfter = null) => {
|
|
498
|
+
const base = buildActionExecutionResult({
|
|
499
|
+
actionType,
|
|
500
|
+
sourceModule: 'interact',
|
|
501
|
+
device,
|
|
502
|
+
selector: targetSelector,
|
|
503
|
+
resolved,
|
|
504
|
+
success: false,
|
|
505
|
+
uiFingerprintBefore: fingerprintBefore,
|
|
506
|
+
uiFingerprintAfter,
|
|
507
|
+
failure: { failureCode, retryable },
|
|
508
|
+
details: {
|
|
509
|
+
target_value: targetValue,
|
|
510
|
+
tolerance: normalizedTolerance,
|
|
511
|
+
property,
|
|
512
|
+
attempts,
|
|
513
|
+
adjustment_mode: adjustmentMode,
|
|
514
|
+
actual_state: actualState,
|
|
515
|
+
converged: false,
|
|
516
|
+
within_tolerance: false,
|
|
517
|
+
reason
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
return {
|
|
521
|
+
...base,
|
|
522
|
+
target_state: {
|
|
523
|
+
property,
|
|
524
|
+
target_value: targetValue,
|
|
525
|
+
tolerance: normalizedTolerance
|
|
526
|
+
},
|
|
527
|
+
actual_state: actualState,
|
|
528
|
+
within_tolerance: false,
|
|
529
|
+
converged: false,
|
|
530
|
+
attempts,
|
|
531
|
+
adjustment_mode: adjustmentMode
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
const resolveCurrentMatch = async () => {
|
|
535
|
+
const tree = await ToolsObserve.getUITreeHandler({ platform: resolvedPlatform, deviceId: resolvedDeviceId });
|
|
536
|
+
resolvedPlatform = tree?.device?.platform === 'ios' ? 'ios' : resolvedPlatform;
|
|
537
|
+
resolvedDeviceId = tree?.device?.id || resolvedDeviceId;
|
|
538
|
+
const elements = Array.isArray(tree?.elements) ? tree.elements : [];
|
|
539
|
+
if (element_id) {
|
|
540
|
+
const stored = ToolsInteract._resolvedUiElements.get(element_id);
|
|
541
|
+
if (!stored) {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
const current = ToolsInteract._findCurrentResolvedElement(elements, resolvedPlatform, resolvedDeviceId, stored);
|
|
545
|
+
if (!current) {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
tree,
|
|
550
|
+
device: tree?.device,
|
|
551
|
+
match: { el: current.el, idx: current.index },
|
|
552
|
+
resolvedTarget: ToolsInteract._resolvedTargetFromElement(ToolsInteract._computeElementId(resolvedPlatform, resolvedDeviceId, current.el, current.index), current.el, current.index)
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
if (semanticFallbackElement) {
|
|
556
|
+
const fallbackBounds = ToolsInteract._normalizeBounds(Array.isArray(semanticFallbackElement.bounds)
|
|
557
|
+
? semanticFallbackElement.bounds
|
|
558
|
+
: semanticFallbackElement.bounds && typeof semanticFallbackElement.bounds === 'object'
|
|
559
|
+
? [
|
|
560
|
+
Number(semanticFallbackElement.bounds.left),
|
|
561
|
+
Number(semanticFallbackElement.bounds.top),
|
|
562
|
+
Number(semanticFallbackElement.bounds.right),
|
|
563
|
+
Number(semanticFallbackElement.bounds.bottom)
|
|
564
|
+
]
|
|
565
|
+
: null);
|
|
566
|
+
let matchedIndex = -1;
|
|
567
|
+
if (fallbackBounds) {
|
|
568
|
+
matchedIndex = elements.findIndex((el) => {
|
|
569
|
+
const bounds = ToolsInteract._normalizeBounds(el.bounds);
|
|
570
|
+
return !!bounds && bounds[0] === fallbackBounds[0] && bounds[1] === fallbackBounds[1] && bounds[2] === fallbackBounds[2] && bounds[3] === fallbackBounds[3];
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
if (matchedIndex === -1 && fallbackBounds) {
|
|
574
|
+
const fallbackCenterX = Math.floor((fallbackBounds[0] + fallbackBounds[2]) / 2);
|
|
575
|
+
const fallbackCenterY = Math.floor((fallbackBounds[1] + fallbackBounds[3]) / 2);
|
|
576
|
+
let bestDistance = Infinity;
|
|
577
|
+
for (let i = 0; i < elements.length; i++) {
|
|
578
|
+
const el = elements[i];
|
|
579
|
+
if (!ToolsInteract._isAdjustableControl(el))
|
|
580
|
+
continue;
|
|
581
|
+
const bounds = ToolsInteract._normalizeBounds(el.bounds);
|
|
582
|
+
if (!bounds)
|
|
583
|
+
continue;
|
|
584
|
+
const centerX = Math.floor((bounds[0] + bounds[2]) / 2);
|
|
585
|
+
const centerY = Math.floor((bounds[1] + bounds[3]) / 2);
|
|
586
|
+
const distance = Math.abs(centerX - fallbackCenterX) + Math.abs(centerY - fallbackCenterY);
|
|
587
|
+
if (distance < bestDistance) {
|
|
588
|
+
bestDistance = distance;
|
|
589
|
+
matchedIndex = i;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (matchedIndex >= 0 && elements[matchedIndex]) {
|
|
594
|
+
const matched = { el: elements[matchedIndex], idx: matchedIndex };
|
|
595
|
+
return {
|
|
596
|
+
tree,
|
|
597
|
+
device: tree?.device,
|
|
598
|
+
match: matched,
|
|
599
|
+
resolvedTarget: ToolsInteract._resolvedTargetFromElement(ToolsInteract._computeElementId(resolvedPlatform, resolvedDeviceId, matched.el, matched.idx), matched.el, matched.idx)
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (selector) {
|
|
604
|
+
const matched = ToolsInteract._findFirstMatchingElement(elements, selector);
|
|
605
|
+
if (!matched) {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
tree,
|
|
610
|
+
device: tree?.device,
|
|
611
|
+
match: matched,
|
|
612
|
+
resolvedTarget: ToolsInteract._resolvedTargetFromElement(ToolsInteract._computeElementId(resolvedPlatform, resolvedDeviceId, matched.el, matched.idx), matched.el, matched.idx)
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
return null;
|
|
616
|
+
};
|
|
617
|
+
if (!selector && !element_id) {
|
|
618
|
+
return buildFailure('ELEMENT_NOT_FOUND', 'selector or element_id is required', null, undefined, null, 0, 'gesture', false);
|
|
619
|
+
}
|
|
620
|
+
if (selector && !element_id) {
|
|
621
|
+
const waitResult = await ToolsInteract.waitForUIHandler({
|
|
622
|
+
selector,
|
|
623
|
+
condition: 'clickable',
|
|
624
|
+
timeout_ms: 5000,
|
|
625
|
+
poll_interval_ms: 300,
|
|
626
|
+
platform: resolvedPlatform,
|
|
627
|
+
deviceId: resolvedDeviceId
|
|
628
|
+
});
|
|
629
|
+
if (waitResult?.status !== 'success' || !waitResult?.element?.elementId) {
|
|
630
|
+
const semanticQuery = selector.text ?? selector.resource_id ?? selector.accessibility_id ?? '';
|
|
631
|
+
if (!semanticQuery) {
|
|
632
|
+
return buildFailure(waitResult?.error?.code === 'ELEMENT_NOT_FOUND' ? 'ELEMENT_NOT_FOUND' : 'TIMEOUT', waitResult?.error?.message ?? 'adjustable control not found', null, waitResult?.device, null, 0, 'gesture', waitResult?.error?.code === 'ELEMENT_NOT_FOUND');
|
|
633
|
+
}
|
|
634
|
+
const fallback = await ToolsInteract.findElementHandler({
|
|
635
|
+
query: semanticQuery,
|
|
636
|
+
exact: false,
|
|
637
|
+
timeoutMs: 3000,
|
|
638
|
+
platform: resolvedPlatform,
|
|
639
|
+
deviceId: resolvedDeviceId
|
|
640
|
+
});
|
|
641
|
+
if (!fallback.found || !fallback.element) {
|
|
642
|
+
return buildFailure('ELEMENT_NOT_FOUND', waitResult?.error?.message ?? 'adjustable control not found', null, waitResult?.device, null, 0, 'gesture', true);
|
|
643
|
+
}
|
|
644
|
+
semanticFallbackElement = fallback.element;
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
element_id = waitResult.element.elementId;
|
|
648
|
+
semanticFallbackElement = null;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
let lastObservedState = null;
|
|
652
|
+
let lastAdjustmentMode = 'gesture';
|
|
653
|
+
let resolvedTarget = null;
|
|
654
|
+
let currentDevice = undefined;
|
|
655
|
+
let attemptCount = 0;
|
|
656
|
+
let cachedResolvedMatch = null;
|
|
657
|
+
for (let attempt = 0; attempt < attemptsLimit; attempt++) {
|
|
658
|
+
const resolved = cachedResolvedMatch
|
|
659
|
+
? {
|
|
660
|
+
tree: null,
|
|
661
|
+
device: currentDevice,
|
|
662
|
+
match: cachedResolvedMatch,
|
|
663
|
+
resolvedTarget: ToolsInteract._resolvedTargetFromElement(ToolsInteract._computeElementId(resolvedPlatform, resolvedDeviceId, cachedResolvedMatch.el, cachedResolvedMatch.idx), cachedResolvedMatch.el, cachedResolvedMatch.idx)
|
|
664
|
+
}
|
|
665
|
+
: await resolveCurrentMatch();
|
|
666
|
+
if (!resolved || !resolved.match || !resolved.resolvedTarget) {
|
|
667
|
+
return buildFailure('STALE_REFERENCE', 'adjustable control could not be resolved', resolvedTarget, currentDevice, lastObservedState, attemptCount, lastAdjustmentMode, true);
|
|
668
|
+
}
|
|
669
|
+
currentDevice = resolved.device;
|
|
670
|
+
resolvedTarget = resolved.resolvedTarget;
|
|
671
|
+
const currentEl = resolved.match.el;
|
|
672
|
+
cachedResolvedMatch = resolved.match;
|
|
673
|
+
const bounds = ToolsInteract._normalizeBounds(currentEl.bounds);
|
|
674
|
+
const valueRange = currentEl.state?.value_range ?? null;
|
|
675
|
+
const currentValue = ToolsInteract._readNumericControlValue(currentEl, property);
|
|
676
|
+
const actualState = currentValue !== null
|
|
677
|
+
? { property, value: currentValue, raw_value: typeof currentEl.state?.raw_value === 'number' ? currentEl.state.raw_value : undefined }
|
|
678
|
+
: null;
|
|
679
|
+
lastObservedState = actualState;
|
|
680
|
+
if (property !== 'value' && property !== 'raw_value') {
|
|
681
|
+
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'adjust_control currently supports numeric value and raw_value properties only', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
682
|
+
}
|
|
683
|
+
if (currentValue !== null && Math.abs(currentValue - targetValue) <= normalizedTolerance) {
|
|
684
|
+
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
685
|
+
const base = buildActionExecutionResult({
|
|
686
|
+
actionType,
|
|
687
|
+
sourceModule: 'interact',
|
|
688
|
+
device: currentDevice,
|
|
689
|
+
selector: targetSelector,
|
|
690
|
+
resolved: resolvedTarget,
|
|
691
|
+
success: true,
|
|
692
|
+
uiFingerprintBefore: fingerprintBefore,
|
|
693
|
+
uiFingerprintAfter,
|
|
694
|
+
details: {
|
|
695
|
+
target_value: targetValue,
|
|
696
|
+
tolerance: normalizedTolerance,
|
|
697
|
+
property,
|
|
698
|
+
attempts: attemptCount,
|
|
699
|
+
adjustment_mode: 'semantic',
|
|
700
|
+
actual_state: actualState,
|
|
701
|
+
converged: true,
|
|
702
|
+
within_tolerance: true,
|
|
703
|
+
reason: 'control already within tolerance'
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
return {
|
|
707
|
+
...base,
|
|
708
|
+
target_state: {
|
|
709
|
+
property,
|
|
710
|
+
target_value: targetValue,
|
|
711
|
+
tolerance: normalizedTolerance
|
|
712
|
+
},
|
|
713
|
+
actual_state: actualState,
|
|
714
|
+
within_tolerance: true,
|
|
715
|
+
converged: true,
|
|
716
|
+
attempts: attemptCount,
|
|
717
|
+
adjustment_mode: 'semantic'
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
if (!ToolsInteract._isAdjustableControl(currentEl)) {
|
|
721
|
+
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'target is not an adjustable control', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
722
|
+
}
|
|
723
|
+
if (!bounds) {
|
|
724
|
+
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'adjustable control has no bounds', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
725
|
+
}
|
|
726
|
+
const min = typeof valueRange?.min === 'number' ? valueRange.min : null;
|
|
727
|
+
const max = typeof valueRange?.max === 'number' ? valueRange.max : null;
|
|
728
|
+
if (min === null || max === null || !Number.isFinite(min) || !Number.isFinite(max) || max <= min) {
|
|
729
|
+
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'value_range unavailable', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
730
|
+
}
|
|
731
|
+
if (targetValue < min || targetValue > max) {
|
|
732
|
+
return buildFailure('UNKNOWN', `targetValue ${targetValue} is outside the control range ${min}..${max}`, resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
733
|
+
}
|
|
734
|
+
const axis = ToolsInteract._controlAxis(currentEl, bounds);
|
|
735
|
+
const targetPoint = ToolsInteract._buildConservativeControlPoint(bounds, targetValue, currentValue, min, max, axis);
|
|
736
|
+
const currentPoint = currentValue !== null
|
|
737
|
+
? ToolsInteract._buildControlPoint(bounds, (currentValue - min) / (max - min), axis)
|
|
738
|
+
: ToolsInteract._buildControlPoint(bounds, 0.5, axis);
|
|
739
|
+
const runVerification = async () => {
|
|
740
|
+
const verification = await ToolsInteract.expectStateHandler({
|
|
741
|
+
element_id: resolvedTarget?.elementId ?? element_id,
|
|
742
|
+
selector: selector ?? undefined,
|
|
743
|
+
property,
|
|
744
|
+
expected: targetValue,
|
|
745
|
+
platform: resolvedPlatform,
|
|
746
|
+
deviceId: resolvedDeviceId
|
|
747
|
+
});
|
|
748
|
+
const observedValue = typeof verification?.observed_state?.value === 'number'
|
|
749
|
+
? verification.observed_state.value
|
|
750
|
+
: typeof verification?.observed_state?.raw_value === 'number'
|
|
751
|
+
? verification.observed_state.raw_value
|
|
752
|
+
: null;
|
|
753
|
+
const observedState = observedValue !== null
|
|
754
|
+
? {
|
|
755
|
+
property,
|
|
756
|
+
value: observedValue,
|
|
757
|
+
raw_value: typeof verification?.observed_state?.raw_value === 'number' ? verification.observed_state.raw_value : undefined
|
|
758
|
+
}
|
|
759
|
+
: actualState;
|
|
760
|
+
return {
|
|
761
|
+
verification,
|
|
762
|
+
observedState,
|
|
763
|
+
withinTolerance: observedValue !== null && Math.abs(observedValue - targetValue) <= normalizedTolerance
|
|
764
|
+
};
|
|
765
|
+
};
|
|
766
|
+
lastAdjustmentMode = 'coordinate';
|
|
767
|
+
const primaryActionResult = await ToolsInteract.tapHandler({
|
|
768
|
+
platform: resolvedPlatform,
|
|
769
|
+
x: targetPoint.x,
|
|
770
|
+
y: targetPoint.y,
|
|
771
|
+
deviceId: resolvedDeviceId
|
|
772
|
+
});
|
|
773
|
+
let actionDevice = primaryActionResult.device ?? currentDevice;
|
|
774
|
+
attemptCount++;
|
|
775
|
+
if (!primaryActionResult.success) {
|
|
776
|
+
lastAdjustmentMode = 'gesture';
|
|
777
|
+
const fallbackActionResult = await ToolsInteract.swipeHandler({
|
|
778
|
+
platform: resolvedPlatform,
|
|
779
|
+
x1: currentPoint.x,
|
|
780
|
+
y1: currentPoint.y,
|
|
781
|
+
x2: targetPoint.x,
|
|
782
|
+
y2: targetPoint.y,
|
|
783
|
+
duration: 220,
|
|
784
|
+
deviceId: resolvedDeviceId
|
|
785
|
+
});
|
|
786
|
+
attemptCount++;
|
|
787
|
+
if (!fallbackActionResult.success) {
|
|
788
|
+
return buildFailure('UNKNOWN', fallbackActionResult.error ?? primaryActionResult.error ?? 'adjustment gesture failed', resolvedTarget, fallbackActionResult.device ?? primaryActionResult.device, actualState, attemptCount, lastAdjustmentMode, false);
|
|
789
|
+
}
|
|
790
|
+
actionDevice = fallbackActionResult.device ?? actionDevice;
|
|
791
|
+
}
|
|
792
|
+
let verificationResult = await runVerification();
|
|
793
|
+
let observedState = verificationResult.observedState;
|
|
794
|
+
lastObservedState = observedState;
|
|
795
|
+
if (!verificationResult.withinTolerance && currentValue !== null) {
|
|
796
|
+
lastAdjustmentMode = 'gesture';
|
|
797
|
+
const fallbackActionResult = await ToolsInteract.swipeHandler({
|
|
798
|
+
platform: resolvedPlatform,
|
|
799
|
+
x1: currentPoint.x,
|
|
800
|
+
y1: currentPoint.y,
|
|
801
|
+
x2: targetPoint.x,
|
|
802
|
+
y2: targetPoint.y,
|
|
803
|
+
duration: 220,
|
|
804
|
+
deviceId: resolvedDeviceId
|
|
805
|
+
});
|
|
806
|
+
attemptCount++;
|
|
807
|
+
if (!fallbackActionResult.success) {
|
|
808
|
+
return buildFailure('UNKNOWN', fallbackActionResult.error ?? 'adjustment gesture failed', resolvedTarget, fallbackActionResult.device, observedState ?? actualState, attemptCount, lastAdjustmentMode, false);
|
|
809
|
+
}
|
|
810
|
+
verificationResult = await runVerification();
|
|
811
|
+
observedState = verificationResult.observedState;
|
|
812
|
+
}
|
|
813
|
+
const verification = verificationResult.verification;
|
|
814
|
+
lastObservedState = observedState;
|
|
815
|
+
if (verificationResult.withinTolerance) {
|
|
816
|
+
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
817
|
+
const base = buildActionExecutionResult({
|
|
818
|
+
actionType,
|
|
819
|
+
sourceModule: 'interact',
|
|
820
|
+
device: actionDevice ?? currentDevice,
|
|
821
|
+
selector: targetSelector,
|
|
822
|
+
resolved: resolvedTarget,
|
|
823
|
+
success: true,
|
|
824
|
+
uiFingerprintBefore: fingerprintBefore,
|
|
825
|
+
uiFingerprintAfter,
|
|
826
|
+
details: {
|
|
827
|
+
target_value: targetValue,
|
|
828
|
+
tolerance: normalizedTolerance,
|
|
829
|
+
property,
|
|
830
|
+
attempts: attemptCount,
|
|
831
|
+
adjustment_mode: lastAdjustmentMode,
|
|
832
|
+
actual_state: observedState,
|
|
833
|
+
converged: true,
|
|
834
|
+
within_tolerance: true,
|
|
835
|
+
reason: verification?.reason ?? 'control converged to target value'
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
return {
|
|
839
|
+
...base,
|
|
840
|
+
target_state: {
|
|
841
|
+
property,
|
|
842
|
+
target_value: targetValue,
|
|
843
|
+
tolerance: normalizedTolerance
|
|
844
|
+
},
|
|
845
|
+
actual_state: observedState,
|
|
846
|
+
within_tolerance: true,
|
|
847
|
+
converged: true,
|
|
848
|
+
attempts: attemptCount,
|
|
849
|
+
adjustment_mode: lastAdjustmentMode
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
cachedResolvedMatch = {
|
|
853
|
+
el: {
|
|
854
|
+
...currentEl,
|
|
855
|
+
state: {
|
|
856
|
+
...(currentEl.state ?? null),
|
|
857
|
+
...(observedState ? {
|
|
858
|
+
[observedState.property]: observedState.value,
|
|
859
|
+
raw_value: observedState.raw_value ?? observedState.value
|
|
860
|
+
} : {})
|
|
861
|
+
}
|
|
862
|
+
},
|
|
863
|
+
idx: resolved.match.idx
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
867
|
+
return buildFailure('TIMEOUT', 'control did not converge within the allotted attempts', resolvedTarget, currentDevice, lastObservedState, attemptCount, lastAdjustmentMode, true, uiFingerprintAfter);
|
|
868
|
+
}
|
|
397
869
|
static async swipeHandler({ platform = 'android', x1, y1, x2, y2, duration, deviceId }) {
|
|
398
870
|
const { interact, resolved } = await ToolsInteract.getInteractionService(platform, deviceId);
|
|
399
871
|
return await interact.swipe(x1, y1, x2, y2, duration, resolved.id);
|
|
@@ -419,17 +891,18 @@ export class ToolsInteract {
|
|
|
419
891
|
if (!q)
|
|
420
892
|
return { found: false, error: 'Empty query' };
|
|
421
893
|
let best = null;
|
|
422
|
-
let
|
|
423
|
-
let
|
|
424
|
-
|
|
894
|
+
let bestTree = null;
|
|
895
|
+
let bestIterationCandidates = [];
|
|
896
|
+
let shouldStop = false;
|
|
897
|
+
const scoreElement = (el, idx) => {
|
|
425
898
|
if (!el || !el.visible)
|
|
426
|
-
return
|
|
899
|
+
return null;
|
|
427
900
|
const bounds = el.bounds || [0, 0, 0, 0];
|
|
428
901
|
if (!Array.isArray(bounds) || bounds.length < 4)
|
|
429
|
-
return
|
|
902
|
+
return null;
|
|
430
903
|
const [l, t, r, b] = bounds;
|
|
431
904
|
if (r <= l || b <= t)
|
|
432
|
-
return
|
|
905
|
+
return null;
|
|
433
906
|
// Do not early-return on non-interactable elements — score them so we can locate their clickable ancestor later
|
|
434
907
|
const interactable = !!(el.clickable || el.enabled || el.focusable);
|
|
435
908
|
const text = normalize(el.text ?? el.label ?? el.value ?? '');
|
|
@@ -437,64 +910,98 @@ export class ToolsInteract {
|
|
|
437
910
|
const resourceId = normalize(el.resourceId ?? el.resourceID ?? el.id ?? '');
|
|
438
911
|
const className = normalize(el.type ?? el.class ?? '');
|
|
439
912
|
let score = 0;
|
|
913
|
+
let reason = 'best_scoring_candidate';
|
|
440
914
|
if (exact) {
|
|
441
|
-
if (text && text === q)
|
|
915
|
+
if (text && text === q) {
|
|
442
916
|
score = 1.0;
|
|
443
|
-
|
|
917
|
+
reason = 'exact_text_match';
|
|
918
|
+
}
|
|
919
|
+
else if (content && content === q) {
|
|
444
920
|
score = 0.95;
|
|
921
|
+
reason = 'exact_content_desc_match';
|
|
922
|
+
}
|
|
923
|
+
else if (resourceId && resourceId === q) {
|
|
924
|
+
score = 0.92;
|
|
925
|
+
reason = 'exact_resource_id_match';
|
|
926
|
+
}
|
|
927
|
+
else if (className && className === q) {
|
|
928
|
+
score = 0.3;
|
|
929
|
+
reason = 'exact_class_match';
|
|
930
|
+
}
|
|
445
931
|
}
|
|
446
932
|
else {
|
|
447
|
-
if (text && text === q)
|
|
933
|
+
if (text && text === q) {
|
|
448
934
|
score = 1.0;
|
|
449
|
-
|
|
935
|
+
reason = 'exact_text_match';
|
|
936
|
+
}
|
|
937
|
+
else if (content && content === q) {
|
|
450
938
|
score = 0.95;
|
|
451
|
-
|
|
939
|
+
reason = 'exact_content_desc_match';
|
|
940
|
+
}
|
|
941
|
+
else if (resourceId && resourceId === q) {
|
|
942
|
+
score = 0.92;
|
|
943
|
+
reason = 'exact_resource_id_match';
|
|
944
|
+
}
|
|
945
|
+
else if (text && text.includes(q)) {
|
|
452
946
|
score = 0.6;
|
|
453
|
-
|
|
947
|
+
reason = 'partial_text_match';
|
|
948
|
+
}
|
|
949
|
+
else if (content && content.includes(q)) {
|
|
454
950
|
score = 0.55;
|
|
455
|
-
|
|
951
|
+
reason = 'partial_content_desc_match';
|
|
952
|
+
}
|
|
953
|
+
else if (resourceId && resourceId.includes(q)) {
|
|
456
954
|
score = 0.7;
|
|
457
|
-
|
|
955
|
+
reason = 'partial_resource_id_match';
|
|
956
|
+
}
|
|
957
|
+
else if (className && className.includes(q)) {
|
|
458
958
|
score = 0.3;
|
|
959
|
+
reason = 'partial_class_match';
|
|
960
|
+
}
|
|
459
961
|
}
|
|
460
962
|
if (score > 0 && interactable)
|
|
461
963
|
score += 0.05;
|
|
462
|
-
|
|
964
|
+
if (score <= 0)
|
|
965
|
+
return null;
|
|
966
|
+
return { el, idx, score, reason, interactable };
|
|
463
967
|
};
|
|
464
968
|
while (Date.now() <= deadline) {
|
|
465
969
|
try {
|
|
466
970
|
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId });
|
|
467
|
-
lastTree = tree;
|
|
468
971
|
if (tree && Array.isArray(tree.elements)) {
|
|
469
972
|
const elements = tree.elements;
|
|
973
|
+
const iterationCandidates = [];
|
|
974
|
+
let iterationImprovedBest = false;
|
|
470
975
|
for (let i = 0; i < elements.length; i++) {
|
|
471
976
|
const el = elements[i];
|
|
472
977
|
try {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
978
|
+
const candidate = scoreElement(el, i);
|
|
979
|
+
if (!candidate)
|
|
980
|
+
continue;
|
|
981
|
+
iterationCandidates.push(candidate);
|
|
982
|
+
if (!best || candidate.score > best.score) {
|
|
983
|
+
best = candidate;
|
|
984
|
+
bestTree = tree;
|
|
985
|
+
iterationImprovedBest = true;
|
|
986
|
+
if (best.score >= 0.95) {
|
|
987
|
+
shouldStop = true;
|
|
988
|
+
break;
|
|
481
989
|
}
|
|
482
990
|
}
|
|
483
|
-
if (bestScore >= 0.95)
|
|
484
|
-
break;
|
|
485
991
|
}
|
|
486
992
|
catch (e) {
|
|
487
993
|
console.error('Error scoring element:', e);
|
|
488
994
|
}
|
|
489
995
|
}
|
|
490
|
-
if (
|
|
491
|
-
|
|
996
|
+
if (iterationImprovedBest) {
|
|
997
|
+
bestIterationCandidates = iterationCandidates.slice();
|
|
998
|
+
}
|
|
492
999
|
}
|
|
493
1000
|
}
|
|
494
1001
|
catch (e) {
|
|
495
1002
|
console.error('Error fetching UI tree:', e);
|
|
496
1003
|
}
|
|
497
|
-
if (Date.now() > deadline)
|
|
1004
|
+
if (shouldStop || Date.now() > deadline)
|
|
498
1005
|
break;
|
|
499
1006
|
await new Promise(r => setTimeout(r, 100));
|
|
500
1007
|
}
|
|
@@ -502,17 +1009,17 @@ export class ToolsInteract {
|
|
|
502
1009
|
return { found: false, error: 'Element not found' };
|
|
503
1010
|
// If the best match is not interactable, try to resolve an actionable ancestor.
|
|
504
1011
|
try {
|
|
505
|
-
const elements = (
|
|
506
|
-
const screen =
|
|
1012
|
+
const elements = (bestTree && Array.isArray(bestTree.elements)) ? bestTree.elements : [];
|
|
1013
|
+
const screen = bestTree?.resolution && typeof bestTree.resolution === 'object' ? bestTree.resolution : null;
|
|
507
1014
|
let chosen = best;
|
|
508
|
-
const childBounds = Array.isArray(chosen?.bounds) ? chosen.bounds : null;
|
|
1015
|
+
const childBounds = Array.isArray(chosen?.el?.bounds) ? chosen.el.bounds : null;
|
|
509
1016
|
// Strategy 1: if parentId references an index, climb that chain
|
|
510
1017
|
let resolvedAncestor = null;
|
|
511
|
-
if (childBounds && (chosen.parentId !== undefined && chosen.parentId !== null)) {
|
|
1018
|
+
if (childBounds && (chosen.el.parentId !== undefined && chosen.el.parentId !== null)) {
|
|
512
1019
|
let cur = chosen;
|
|
513
1020
|
let safety = 0;
|
|
514
|
-
while (cur && safety < 20 && !(cur.clickable || cur.focusable) && (cur.parentId !== undefined && cur.parentId !== null)) {
|
|
515
|
-
let pid = cur.parentId;
|
|
1021
|
+
while (cur && safety < 20 && !(cur.el.clickable || cur.el.focusable) && (cur.el.parentId !== undefined && cur.el.parentId !== null)) {
|
|
1022
|
+
let pid = cur.el.parentId;
|
|
516
1023
|
let idx = null;
|
|
517
1024
|
if (typeof pid === 'number')
|
|
518
1025
|
idx = pid;
|
|
@@ -520,18 +1027,19 @@ export class ToolsInteract {
|
|
|
520
1027
|
idx = Number(pid);
|
|
521
1028
|
// If parentId is not an index, try to find by matching resourceId or id field
|
|
522
1029
|
if (idx !== null && elements[idx]) {
|
|
523
|
-
cur = elements[idx];
|
|
524
|
-
if (cur && (cur.clickable || cur.enabled || cur.focusable)) {
|
|
1030
|
+
cur = { el: elements[idx], idx };
|
|
1031
|
+
if (cur && (cur.el.clickable || cur.el.enabled || cur.el.focusable)) {
|
|
525
1032
|
resolvedAncestor = cur;
|
|
526
1033
|
break;
|
|
527
1034
|
}
|
|
528
1035
|
}
|
|
529
1036
|
else if (typeof pid === 'string') {
|
|
530
1037
|
// fallback: search elements for matching resourceId or id
|
|
531
|
-
const
|
|
1038
|
+
const foundIndex = elements.findIndex((el) => (el.resourceId === pid || el.id === pid));
|
|
1039
|
+
const found = foundIndex >= 0 ? elements[foundIndex] : null;
|
|
532
1040
|
if (found) {
|
|
533
|
-
cur = found;
|
|
534
|
-
if (cur && (cur.clickable || cur.enabled || cur.focusable)) {
|
|
1041
|
+
cur = { el: found, idx: foundIndex };
|
|
1042
|
+
if (cur && (cur.el.clickable || cur.el.enabled || cur.el.focusable)) {
|
|
535
1043
|
resolvedAncestor = cur;
|
|
536
1044
|
break;
|
|
537
1045
|
}
|
|
@@ -551,16 +1059,19 @@ export class ToolsInteract {
|
|
|
551
1059
|
if (!resolvedAncestor && childBounds) {
|
|
552
1060
|
const [cl, ct, cr, cb] = childBounds;
|
|
553
1061
|
// find candidates that are clickable and contain the child bounds
|
|
554
|
-
const candidates = elements
|
|
1062
|
+
const candidates = elements
|
|
1063
|
+
.map((el, idx) => ({ el, idx }))
|
|
1064
|
+
.filter(({ el }) => el && (el.clickable || el.focusable) && Array.isArray(el.bounds) && el.bounds.length >= 4);
|
|
555
1065
|
let bestCandidate = null;
|
|
556
1066
|
let bestCandidateArea = Infinity;
|
|
557
1067
|
for (const c of candidates) {
|
|
558
|
-
const
|
|
1068
|
+
const bounds = c.el.bounds;
|
|
1069
|
+
const [pl, pt, pr, pb] = bounds;
|
|
559
1070
|
if (pl <= cl && pt <= ct && pr >= cr && pb >= cb) {
|
|
560
1071
|
const area = (pr - pl) * (pb - pt);
|
|
561
1072
|
if (area < bestCandidateArea) {
|
|
562
1073
|
bestCandidateArea = area;
|
|
563
|
-
bestCandidate = c
|
|
1074
|
+
bestCandidate = c;
|
|
564
1075
|
}
|
|
565
1076
|
}
|
|
566
1077
|
}
|
|
@@ -568,17 +1079,24 @@ export class ToolsInteract {
|
|
|
568
1079
|
resolvedAncestor = bestCandidate;
|
|
569
1080
|
}
|
|
570
1081
|
if (resolvedAncestor) {
|
|
571
|
-
best =
|
|
572
|
-
|
|
573
|
-
|
|
1082
|
+
best = {
|
|
1083
|
+
el: resolvedAncestor.el,
|
|
1084
|
+
idx: resolvedAncestor.idx,
|
|
1085
|
+
score: Math.min(1, best.score + 0.02),
|
|
1086
|
+
reason: 'clickable_parent_preferred',
|
|
1087
|
+
interactable: true
|
|
1088
|
+
};
|
|
574
1089
|
}
|
|
575
|
-
if (best && !(best.clickable || best.focusable)) {
|
|
576
|
-
const nearbyActionable = ToolsInteract._resolveNearbyActionableControl(elements, { el: best, idx: best.
|
|
1090
|
+
if (best && !(best.el.clickable || best.el.focusable)) {
|
|
1091
|
+
const nearbyActionable = ToolsInteract._resolveNearbyActionableControl(elements, { el: best.el, idx: best.idx }, screen);
|
|
577
1092
|
if (nearbyActionable) {
|
|
578
|
-
best =
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
1093
|
+
best = {
|
|
1094
|
+
el: nearbyActionable.el,
|
|
1095
|
+
idx: nearbyActionable.idx,
|
|
1096
|
+
score: Math.min(1, best.score + 0.02),
|
|
1097
|
+
reason: nearbyActionable.sliderLike ? 'slider_track_preferred' : 'nearby_actionable_control',
|
|
1098
|
+
interactable: true
|
|
1099
|
+
};
|
|
582
1100
|
}
|
|
583
1101
|
}
|
|
584
1102
|
}
|
|
@@ -587,29 +1105,34 @@ export class ToolsInteract {
|
|
|
587
1105
|
}
|
|
588
1106
|
if (!best)
|
|
589
1107
|
return { found: false, error: 'Element not found' };
|
|
590
|
-
const boundsObj = Array.isArray(best.bounds) ? { left: best.bounds[0], top: best.bounds[1], right: best.bounds[2], bottom: best.bounds[3] } : null;
|
|
1108
|
+
const boundsObj = Array.isArray(best.el.bounds) ? { left: best.el.bounds[0], top: best.el.bounds[1], right: best.el.bounds[2], bottom: best.el.bounds[3] } : null;
|
|
591
1109
|
const tapCoordinates = boundsObj ? { x: Math.floor((boundsObj.left + boundsObj.right) / 2), y: Math.floor((boundsObj.top + boundsObj.bottom) / 2) } : null;
|
|
1110
|
+
const uniqueRanked = bestIterationCandidates.filter((candidate, index, array) => index === array.findIndex((other) => other.idx === candidate.idx && other.el === candidate.el));
|
|
1111
|
+
const alternateCandidates = uniqueRanked
|
|
1112
|
+
.filter((candidate) => candidate.idx !== best.idx || candidate.el !== best.el)
|
|
1113
|
+
.slice(0, 3)
|
|
1114
|
+
.map((candidate) => ToolsInteract._summarizeResolutionCandidate(candidate));
|
|
592
1115
|
const outEl = {
|
|
593
|
-
text: best.text ?? null,
|
|
594
|
-
resourceId: best.resourceId ?? null,
|
|
595
|
-
contentDesc: best.contentDescription ?? best.contentDesc ?? null,
|
|
596
|
-
class: best.type ?? best.class ?? null,
|
|
1116
|
+
text: best.el.text ?? null,
|
|
1117
|
+
resourceId: best.el.resourceId ?? null,
|
|
1118
|
+
contentDesc: best.el.contentDescription ?? best.el.contentDesc ?? null,
|
|
1119
|
+
class: best.el.type ?? best.el.class ?? null,
|
|
597
1120
|
bounds: boundsObj,
|
|
598
|
-
clickable: !!best.clickable,
|
|
599
|
-
enabled: !!best.enabled,
|
|
600
|
-
stable_id: best.stable_id ?? null,
|
|
601
|
-
role: best.role ?? null,
|
|
602
|
-
test_tag: best.test_tag ?? null,
|
|
603
|
-
selector: best.selector ?? null,
|
|
604
|
-
semantic: best.semantic ?? null,
|
|
1121
|
+
clickable: !!best.el.clickable,
|
|
1122
|
+
enabled: !!best.el.enabled,
|
|
1123
|
+
stable_id: best.el.stable_id ?? null,
|
|
1124
|
+
role: best.el.role ?? null,
|
|
1125
|
+
test_tag: best.el.test_tag ?? null,
|
|
1126
|
+
selector: best.el.selector ?? null,
|
|
1127
|
+
semantic: best.el.semantic ?? null,
|
|
605
1128
|
tapCoordinates,
|
|
606
1129
|
telemetry: {
|
|
607
|
-
matchedIndex: best
|
|
608
|
-
matchedInteractable: !!best
|
|
609
|
-
sliderLike:
|
|
1130
|
+
matchedIndex: best.idx ?? null,
|
|
1131
|
+
matchedInteractable: !!best.interactable,
|
|
1132
|
+
sliderLike: best.reason === 'slider_track_preferred'
|
|
610
1133
|
}
|
|
611
1134
|
};
|
|
612
|
-
if (best
|
|
1135
|
+
if (best.reason === 'slider_track_preferred') {
|
|
613
1136
|
const isVertical = !!boundsObj && (boundsObj.bottom - boundsObj.top) > (boundsObj.right - boundsObj.left);
|
|
614
1137
|
const interactionHint = {
|
|
615
1138
|
kind: 'slider',
|
|
@@ -618,8 +1141,15 @@ export class ToolsInteract {
|
|
|
618
1141
|
};
|
|
619
1142
|
outEl.interactionHint = interactionHint;
|
|
620
1143
|
}
|
|
621
|
-
const scoreVal = Math.min(1, Number(
|
|
622
|
-
|
|
1144
|
+
const scoreVal = Math.min(1, Number(best.score.toFixed(3)));
|
|
1145
|
+
const resolution = {
|
|
1146
|
+
confidence: scoreVal,
|
|
1147
|
+
reason: best.reason,
|
|
1148
|
+
fallback_available: alternateCandidates.length > 0,
|
|
1149
|
+
matched_count: uniqueRanked.length,
|
|
1150
|
+
alternates: alternateCandidates
|
|
1151
|
+
};
|
|
1152
|
+
return { found: true, element: outEl, score: scoreVal, confidence: scoreVal, resolution };
|
|
623
1153
|
}
|
|
624
1154
|
static async waitForUIHandler({ selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry = { max_attempts: 1, backoff_ms: 0 }, platform, deviceId }) {
|
|
625
1155
|
const overallStart = Date.now();
|