mobile-debug-mcp 0.30.1 → 0.31.0

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.
Files changed (60) hide show
  1. package/README.md +50 -5
  2. package/dist/interact/index.js +546 -133
  3. package/dist/manage/ios.js +1 -1
  4. package/dist/server/tool-definitions.js +13 -9
  5. package/dist/server/tool-handlers.js +4 -4
  6. package/dist/server-core.js +1 -1
  7. package/dist/system/index.js +485 -48
  8. package/dist/system/ios.js +15 -7
  9. package/dist/utils/android/utils.js +4 -4
  10. package/dist/utils/cli/idb/check-idb.js +10 -3
  11. package/dist/utils/cli/idb/idb-helper.js +47 -13
  12. package/dist/utils/ios/utils.js +5 -9
  13. package/docs/CHANGELOG.md +6 -0
  14. package/docs/ROADMAP.md +88 -212
  15. package/docs/specs/baseline-spec-v0.md +1 -1
  16. package/docs/specs-deltas/015-adjustable-control-support.md +541 -0
  17. package/docs/specs-deltas/016-environment-tools.md +533 -0
  18. package/docs/tools/TOOLS.md +2 -0
  19. package/docs/tools/manage.md +4 -2
  20. package/docs/tools/system.md +9 -2
  21. package/package.json +1 -1
  22. package/skills/spec-review/SKILL.md +52 -0
  23. package/skills/spec-review/references/spec-review-checklist.md +13 -0
  24. package/skills/spec-review/references/spec-review-template.md +28 -0
  25. package/src/interact/index.ts +581 -138
  26. package/src/manage/ios.ts +1 -1
  27. package/src/server/tool-definitions.ts +13 -9
  28. package/src/server/tool-handlers.ts +4 -4
  29. package/src/server-core.ts +1 -1
  30. package/src/system/index.ts +572 -50
  31. package/src/system/ios.ts +14 -7
  32. package/src/types.ts +2 -0
  33. package/src/utils/android/utils.ts +3 -3
  34. package/src/utils/cli/idb/check-idb.ts +11 -2
  35. package/src/utils/cli/idb/idb-helper.ts +49 -12
  36. package/src/utils/ios/utils.ts +4 -8
  37. package/test/unit/interact/adjust_control.test.ts +586 -33
  38. package/test/unit/interact/tap_element.test.ts +1 -1
  39. package/test/unit/server/contract.test.ts +4 -0
  40. package/test/unit/system/get_system_status.test.ts +3 -0
  41. package/test/unit/system/system_status.test.ts +40 -0
  42. package/test/unit/utils/idb_health.test.ts +74 -0
  43. package/skills/rfc-review/SKILL.md +0 -52
  44. package/skills/rfc-review/references/rfc-review-checklist.md +0 -12
  45. package/skills/rfc-review/references/rfc-review-template.md +0 -28
  46. /package/docs/{rfcs → specs-deltas}/001-state-verification.md +0 -0
  47. /package/docs/{rfcs → specs-deltas}/002-richer-element-identity +0 -0
  48. /package/docs/{rfcs → specs-deltas}/003-wait-and-synchronization-reliability.md +0 -0
  49. /package/docs/{rfcs → specs-deltas}/004-action-verification-routing.md +0 -0
  50. /package/docs/{rfcs → specs-deltas}/005-unified-action-execution-and-verification-model.md +0 -0
  51. /package/docs/{rfcs → specs-deltas}/006-runtime-action-instrumentation-and-binding-layer.md +0 -0
  52. /package/docs/{rfcs → specs-deltas}/007-actionability-resolution-and-executable-target-selection.md +0 -0
  53. /package/docs/{rfcs → specs-deltas}/008-adjustable-control-support-and-semantic-value-manipulation.md +0 -0
  54. /package/docs/{rfcs → specs-deltas}/009-semantic-control-modeling-for-custom-and-composite-controls.md +0 -0
  55. /package/docs/{rfcs → specs-deltas}/010-verification-stabilization-and-temporal-convergence.md +0 -0
  56. /package/docs/{rfcs → specs-deltas}/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows.md +0 -0
  57. /package/docs/{rfcs → specs-deltas}/011.1-recovery-contract-types-and-runtime-wiring-spec.md +0 -0
  58. /package/docs/{rfcs → specs-deltas}/012-action-trace-and-xecution-observability.md +0 -0
  59. /package/docs/{rfcs → specs-deltas}/013-wait-and-synchronization-reliability.md +0 -0
  60. /package/docs/{rfcs → specs-deltas}/014-actionability-resolution.md +0 -0
@@ -528,13 +528,214 @@ export class ToolsInteract {
528
528
  const stateValue = el.state[property];
529
529
  if (typeof stateValue === 'number' && Number.isFinite(stateValue))
530
530
  return stateValue;
531
+ const parsedStateValue = ToolsInteract._parseNumericControlValue(stateValue);
532
+ if (parsedStateValue !== null)
533
+ return parsedStateValue;
531
534
  if (property === 'value' || property === 'raw_value') {
532
535
  const fallback = el.state.raw_value ?? el.state.value;
533
536
  if (typeof fallback === 'number' && Number.isFinite(fallback))
534
537
  return fallback;
538
+ return ToolsInteract._parseNumericControlValue(fallback);
535
539
  }
536
540
  return null;
537
541
  }
542
+ static _parseNumericControlValue(value) {
543
+ if (typeof value === 'number' && Number.isFinite(value))
544
+ return value;
545
+ if (typeof value !== 'string')
546
+ return null;
547
+ const trimmed = value.trim();
548
+ if (!trimmed)
549
+ return null;
550
+ const percent = trimmed.match(/^([-+]?\d+(?:\.\d+)?)\s*%$/);
551
+ if (percent)
552
+ return Number(percent[1]);
553
+ const fraction = trimmed.match(/^([-+]?\d+(?:\.\d+)?)\s+of\s+[-+]?\d+(?:\.\d+)?$/i);
554
+ if (fraction)
555
+ return Number(fraction[1]);
556
+ const plain = Number(trimmed);
557
+ if (Number.isFinite(plain))
558
+ return plain;
559
+ const trailing = trimmed.match(/,\s*([-+]?\d+(?:\.\d+)?)$/);
560
+ if (trailing)
561
+ return Number(trailing[1]);
562
+ return null;
563
+ }
564
+ static _readNumericControlRange(el) {
565
+ const min = typeof el?.state?.value_range?.min === 'number' ? el.state.value_range.min : null;
566
+ const max = typeof el?.state?.value_range?.max === 'number' ? el.state.value_range.max : null;
567
+ if (min !== null && max !== null && Number.isFinite(min) && Number.isFinite(max) && max > min) {
568
+ return { min, max };
569
+ }
570
+ const value = el?.state?.value ?? el?.state?.raw_value;
571
+ if (typeof value === 'string') {
572
+ const fraction = value.trim().match(/^[-+]?\d+(?:\.\d+)?\s+of\s+([-+]?\d+(?:\.\d+)?)$/i);
573
+ if (fraction) {
574
+ const parsedMax = Number(fraction[1]);
575
+ if (Number.isFinite(parsedMax) && parsedMax > 0)
576
+ return { min: 0, max: parsedMax };
577
+ }
578
+ }
579
+ return null;
580
+ }
581
+ static _readControlStep(el) {
582
+ const candidates = [
583
+ el?.state?.step,
584
+ el?.state?.value_range?.step
585
+ ];
586
+ for (const candidate of candidates) {
587
+ if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0)
588
+ return candidate;
589
+ }
590
+ return null;
591
+ }
592
+ static _quantizeTargetValue(targetValue, min, max, step) {
593
+ if (!step)
594
+ return targetValue;
595
+ const quantized = min + (Math.round((targetValue - min) / step) * step);
596
+ const bounded = Math.max(min, Math.min(max, quantized));
597
+ const precision = ToolsInteract._decimalPrecisionForStep(step);
598
+ return Number(bounded.toFixed(precision));
599
+ }
600
+ static _decimalPrecisionForStep(step) {
601
+ const normalized = String(step).toLowerCase();
602
+ const [coefficient, exponentPart] = normalized.split('e');
603
+ const coefficientDecimals = coefficient.includes('.')
604
+ ? coefficient.split('.')[1]?.length ?? 0
605
+ : 0;
606
+ const exponent = exponentPart !== undefined ? Number(exponentPart) : 0;
607
+ const precision = exponent < 0
608
+ ? coefficientDecimals + Math.abs(exponent)
609
+ : Math.max(0, coefficientDecimals - exponent);
610
+ return Math.max(0, Math.min(12, precision));
611
+ }
612
+ static _defaultAdjustTolerance(range, step) {
613
+ if (step)
614
+ return 0;
615
+ if (!range)
616
+ return 0;
617
+ return Math.max(0, 0.01 * Math.abs(range.max - range.min));
618
+ }
619
+ static _runtimeActionSet(el) {
620
+ const values = [
621
+ ...(Array.isArray(el?.actions) ? el.actions : []),
622
+ ...(Array.isArray(el?.accessibilityActions) ? el.accessibilityActions : []),
623
+ ...(Array.isArray(el?.runtimeActions) ? el.runtimeActions : []),
624
+ ...(Array.isArray(el?.platformActions) ? el.platformActions : [])
625
+ ];
626
+ return new Set(values.map((value) => ToolsInteract._normalize(value)).filter(Boolean));
627
+ }
628
+ static _hasDirectAdjustmentAction(el) {
629
+ const actions = ToolsInteract._runtimeActionSet(el);
630
+ return [
631
+ 'action_set_progress',
632
+ 'set_progress',
633
+ 'setprogress',
634
+ 'semanticsactions.setprogress',
635
+ 'set_value',
636
+ 'setvalue',
637
+ 'direct_set_value'
638
+ ].some((action) => actions.has(action));
639
+ }
640
+ static _adjustmentDirections(el) {
641
+ const actions = ToolsInteract._runtimeActionSet(el);
642
+ return {
643
+ increment: ['increment', 'accessibility_increment', 'action_increment'].some((action) => actions.has(action)),
644
+ decrement: ['decrement', 'accessibility_decrement', 'action_decrement'].some((action) => actions.has(action))
645
+ };
646
+ }
647
+ static _textFromElement(el) {
648
+ return String(el?.text ?? el?.contentDescription ?? el?.contentDesc ?? el?.accessibilityLabel ?? el?.value ?? '').trim();
649
+ }
650
+ static _parseLeadingNumber(text) {
651
+ const match = text.trim().match(/^([-+]?\d+(?:\.\d+)?)(?:\s*[a-zA-Z%]+)?$/);
652
+ if (!match)
653
+ return null;
654
+ const value = Number(match[1]);
655
+ return Number.isFinite(value) ? value : null;
656
+ }
657
+ static _inferLabeledSliderState(elements, track) {
658
+ const trackBounds = ToolsInteract._normalizeBounds(track?.bounds);
659
+ if (!track || !trackBounds)
660
+ return null;
661
+ const type = ToolsInteract._normalize(track.type ?? track.class ?? '');
662
+ const role = ToolsInteract._normalize(track.role ?? '');
663
+ const width = trackBounds[2] - trackBounds[0];
664
+ const height = trackBounds[3] - trackBounds[1];
665
+ const wideTrackLike = width >= 240 && height <= 220;
666
+ const knownTrackLike = /slider|seekbar|range/.test(type) || /slider|range/.test(role);
667
+ if (!wideTrackLike && !knownTrackLike)
668
+ return null;
669
+ const trackTop = trackBounds[1];
670
+ const trackBottom = trackBounds[3];
671
+ const trackCenterX = (trackBounds[0] + trackBounds[2]) / 2;
672
+ const labels = elements
673
+ .map((el, index) => {
674
+ const bounds = ToolsInteract._normalizeBounds(el.bounds);
675
+ const text = ToolsInteract._textFromElement(el);
676
+ const value = ToolsInteract._parseLeadingNumber(text);
677
+ return bounds && text && value !== null
678
+ ? {
679
+ index,
680
+ el,
681
+ text,
682
+ value,
683
+ bounds,
684
+ centerX: (bounds[0] + bounds[2]) / 2,
685
+ centerY: (bounds[1] + bounds[3]) / 2,
686
+ hasUnit: /[a-zA-Z%]/.test(text)
687
+ }
688
+ : null;
689
+ })
690
+ .filter((label) => !!label);
691
+ const rangeLabels = labels
692
+ .filter((label) => {
693
+ const verticallyNearTrack = label.centerY >= trackTop - 520 && label.centerY <= trackBottom + 260;
694
+ const horizontallyNearTrack = label.centerX >= trackBounds[0] - 120 && label.centerX <= trackBounds[2] + 120;
695
+ return verticallyNearTrack && horizontallyNearTrack && label.hasUnit;
696
+ })
697
+ .sort((a, b) => a.centerX - b.centerX);
698
+ if (rangeLabels.length < 2)
699
+ return null;
700
+ const minLabel = rangeLabels[0];
701
+ const maxLabel = rangeLabels[rangeLabels.length - 1];
702
+ if (!minLabel || !maxLabel || maxLabel.value <= minLabel.value)
703
+ return null;
704
+ const currentCandidates = labels
705
+ .filter((label) => {
706
+ if (label.index === minLabel.index || label.index === maxLabel.index)
707
+ return false;
708
+ if (label.value < minLabel.value || label.value > maxLabel.value)
709
+ return false;
710
+ if (label.centerY < trackTop - 950 || label.centerY > minLabel.centerY - 20)
711
+ return false;
712
+ return label.centerX >= trackCenterX - 80;
713
+ })
714
+ .sort((a, b) => {
715
+ const yDistance = Math.abs((trackTop - 420) - a.centerY) - Math.abs((trackTop - 420) - b.centerY);
716
+ if (yDistance !== 0)
717
+ return yDistance;
718
+ return b.centerX - a.centerX;
719
+ });
720
+ const currentLabel = currentCandidates[0];
721
+ if (!currentLabel)
722
+ return null;
723
+ const candidateBelowRangeLabels = minLabel.centerY < trackTop && maxLabel.centerY < trackTop;
724
+ const labelDerivedBounds = [
725
+ Math.min(minLabel.bounds[0], trackBounds[0]),
726
+ Math.max(currentLabel.bounds[3] + 12, Math.min(minLabel.bounds[1], maxLabel.bounds[1]) - 160),
727
+ Math.max(maxLabel.bounds[2], trackBounds[2]),
728
+ Math.max(currentLabel.bounds[3] + 80, Math.min(minLabel.bounds[1], maxLabel.bounds[1]) - 32)
729
+ ];
730
+ return {
731
+ current: currentLabel.value,
732
+ min: minLabel.value,
733
+ max: maxLabel.value,
734
+ value_range: { min: minLabel.value, max: maxLabel.value },
735
+ touch_bounds: candidateBelowRangeLabels ? labelDerivedBounds : trackBounds,
736
+ source: 'nearby_labels'
737
+ };
738
+ }
538
739
  static _buildControlPoint(bounds, ratio, axis) {
539
740
  const clampedRatio = Math.max(0, Math.min(1, ratio));
540
741
  const [left, top, right, bottom] = bounds;
@@ -797,11 +998,13 @@ export class ToolsInteract {
797
998
  sourceModule: 'interact'
798
999
  });
799
1000
  }
800
- static async adjustControlHandler({ selector, element_id, property = 'value', targetValue, tolerance = 0, maxAttempts = 3, platform, deviceId }) {
1001
+ static async adjustControlHandler({ selector, element_id, property = 'value', targetValue, tolerance, maxAttempts, platform, deviceId }) {
801
1002
  const actionType = 'adjust_control';
802
1003
  const targetSelector = selector ?? (element_id ? { elementId: element_id } : null);
803
- const normalizedTolerance = Number.isFinite(tolerance) ? Math.max(0, tolerance) : 0;
804
- const attemptsLimit = Math.max(1, Math.floor(Number(maxAttempts) || 1));
1004
+ const requestedTolerance = typeof tolerance === 'number' && Number.isFinite(tolerance) ? Math.max(0, tolerance) : null;
1005
+ const attemptsLimit = maxAttempts === undefined || maxAttempts === null
1006
+ ? 5
1007
+ : Math.max(1, Math.floor(Number(maxAttempts) || 1));
805
1008
  const sourcePlatform = platform || 'android';
806
1009
  let resolvedPlatform = sourcePlatform;
807
1010
  let resolvedDeviceId = deviceId;
@@ -835,6 +1038,7 @@ export class ToolsInteract {
835
1038
  retry_depth: attempts
836
1039
  });
837
1040
  }
1041
+ const responseTolerance = requestedTolerance ?? 0;
838
1042
  const base = buildActionExecutionResult({
839
1043
  actionType,
840
1044
  sourceModule: 'interact',
@@ -847,7 +1051,7 @@ export class ToolsInteract {
847
1051
  failure: { failureCode, retryable },
848
1052
  details: {
849
1053
  target_value: targetValue,
850
- tolerance: normalizedTolerance,
1054
+ tolerance: responseTolerance,
851
1055
  property,
852
1056
  attempts,
853
1057
  adjustment_mode: adjustmentMode,
@@ -863,7 +1067,7 @@ export class ToolsInteract {
863
1067
  target_state: {
864
1068
  property,
865
1069
  target_value: targetValue,
866
- tolerance: normalizedTolerance
1070
+ tolerance: responseTolerance
867
1071
  },
868
1072
  actual_state: actualState,
869
1073
  within_tolerance: false,
@@ -955,8 +1159,8 @@ export class ToolsInteract {
955
1159
  }
956
1160
  return null;
957
1161
  };
958
- if (!selector && !element_id) {
959
- return buildFailure('ELEMENT_NOT_FOUND', 'selector or element_id is required', null, undefined, null, 0, 'gesture', false);
1162
+ if ((!selector && !element_id) || (selector && element_id)) {
1163
+ return buildFailure('ELEMENT_NOT_FOUND', 'exactly one of selector or element_id is required', null, undefined, null, 0, 'gesture', false);
960
1164
  }
961
1165
  if (selector && !element_id) {
962
1166
  const waitResult = await ToolsInteract.waitForUIHandler({
@@ -995,7 +1199,8 @@ export class ToolsInteract {
995
1199
  let currentDevice = undefined;
996
1200
  let attemptCount = 0;
997
1201
  let cachedResolvedMatch = null;
998
- for (let attempt = 0; attempt < attemptsLimit; attempt++) {
1202
+ let coordinateTapsAwaitingFreshGesture = false;
1203
+ adjustmentAttempts: for (let attempt = 0; attempt < attemptsLimit && attemptCount < attemptsLimit; attempt++) {
999
1204
  const resolved = cachedResolvedMatch
1000
1205
  ? {
1001
1206
  tree: null,
@@ -1015,11 +1220,29 @@ export class ToolsInteract {
1015
1220
  if (!adjustableActionability.actionable) {
1016
1221
  return buildFailure(adjustableActionability.failureCode ?? 'ELEMENT_NOT_INTERACTABLE', adjustableActionability.reason ?? 'adjustable control is not actionable', resolvedTarget, currentDevice, lastObservedState, attemptCount, lastAdjustmentMode, true);
1017
1222
  }
1018
- const bounds = ToolsInteract._normalizeBounds(currentEl.bounds);
1019
- const valueRange = currentEl.state?.value_range ?? null;
1020
- const currentValue = ToolsInteract._readNumericControlValue(currentEl, property);
1223
+ const elementsForInference = Array.isArray(resolved.tree?.elements)
1224
+ ? resolved.tree.elements
1225
+ : cachedResolvedMatch
1226
+ ? [cachedResolvedMatch.el]
1227
+ : [currentEl];
1228
+ const inferredSliderState = ToolsInteract._inferLabeledSliderState(elementsForInference, currentEl);
1229
+ const nativeBounds = ToolsInteract._normalizeBounds(currentEl.bounds);
1230
+ const bounds = inferredSliderState?.touch_bounds ?? nativeBounds;
1231
+ const valueRange = ToolsInteract._readNumericControlRange(currentEl) ?? inferredSliderState?.value_range ?? null;
1232
+ const step = ToolsInteract._readControlStep(currentEl);
1233
+ const normalizedTargetValue = valueRange
1234
+ ? ToolsInteract._quantizeTargetValue(targetValue, valueRange.min, valueRange.max, step)
1235
+ : targetValue;
1236
+ const effectiveTolerance = requestedTolerance ?? ToolsInteract._defaultAdjustTolerance(valueRange, step);
1237
+ const currentValue = ToolsInteract._readNumericControlValue(currentEl, property) ?? inferredSliderState?.current ?? null;
1021
1238
  const actualState = currentValue !== null
1022
- ? { property, value: currentValue, raw_value: typeof currentEl.state?.raw_value === 'number' ? currentEl.state.raw_value : undefined }
1239
+ ? {
1240
+ property,
1241
+ value: currentValue,
1242
+ raw_value: typeof currentEl.state?.raw_value === 'number'
1243
+ ? currentEl.state.raw_value
1244
+ : inferredSliderState?.current
1245
+ }
1023
1246
  : null;
1024
1247
  lastObservedState = actualState;
1025
1248
  if (!traceSteps.some((step) => step.stage === 'resolve')) {
@@ -1032,10 +1255,20 @@ export class ToolsInteract {
1032
1255
  if (property !== 'value' && property !== 'raw_value') {
1033
1256
  return buildFailure('ELEMENT_NOT_INTERACTABLE', 'adjust_control currently supports numeric value and raw_value properties only', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
1034
1257
  }
1035
- if (currentValue !== null && Math.abs(currentValue - targetValue) <= normalizedTolerance) {
1258
+ const isAdjustableControl = ToolsInteract._isAdjustableControl(currentEl) || !!inferredSliderState;
1259
+ if (!isAdjustableControl) {
1260
+ return buildFailure('ELEMENT_NOT_INTERACTABLE', 'target is not an adjustable control', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
1261
+ }
1262
+ if (currentValue === null) {
1263
+ return buildFailure('UNKNOWN', 'current numeric value is unavailable', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
1264
+ }
1265
+ if (valueRange && (targetValue < valueRange.min || targetValue > valueRange.max)) {
1266
+ return buildFailure('CONTROL_CONVERGENCE_FAILED', `targetValue ${targetValue} is outside the control range ${valueRange.min}..${valueRange.max}`, resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
1267
+ }
1268
+ if (currentValue !== null && Math.abs(currentValue - normalizedTargetValue) <= effectiveTolerance) {
1036
1269
  recordTraceStep('verify', 'success', {
1037
1270
  property,
1038
- target_value: targetValue,
1271
+ target_value: normalizedTargetValue,
1039
1272
  actual_state: actualState,
1040
1273
  reason: 'control already within tolerance'
1041
1274
  });
@@ -1050,8 +1283,8 @@ export class ToolsInteract {
1050
1283
  uiFingerprintBefore: fingerprintBefore,
1051
1284
  uiFingerprintAfter,
1052
1285
  details: {
1053
- target_value: targetValue,
1054
- tolerance: normalizedTolerance,
1286
+ target_value: normalizedTargetValue,
1287
+ tolerance: effectiveTolerance,
1055
1288
  property,
1056
1289
  attempts: attemptCount,
1057
1290
  adjustment_mode: 'semantic',
@@ -1066,8 +1299,8 @@ export class ToolsInteract {
1066
1299
  ...base,
1067
1300
  target_state: {
1068
1301
  property,
1069
- target_value: targetValue,
1070
- tolerance: normalizedTolerance
1302
+ target_value: normalizedTargetValue,
1303
+ tolerance: effectiveTolerance
1071
1304
  },
1072
1305
  actual_state: actualState,
1073
1306
  within_tolerance: true,
@@ -1076,40 +1309,40 @@ export class ToolsInteract {
1076
1309
  adjustment_mode: 'semantic'
1077
1310
  };
1078
1311
  }
1079
- if (!ToolsInteract._isAdjustableControl(currentEl)) {
1080
- return buildFailure('ELEMENT_NOT_INTERACTABLE', 'target is not an adjustable control', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
1081
- }
1082
1312
  if (!bounds) {
1083
1313
  return buildFailure('ELEMENT_NOT_INTERACTABLE', 'adjustable control has no bounds', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
1084
1314
  }
1085
- const min = typeof valueRange?.min === 'number' ? valueRange.min : null;
1086
- const max = typeof valueRange?.max === 'number' ? valueRange.max : null;
1087
- if (min === null || max === null || !Number.isFinite(min) || !Number.isFinite(max) || max <= min) {
1088
- return buildFailure('ELEMENT_NOT_INTERACTABLE', 'value_range unavailable', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
1089
- }
1090
- if (targetValue < min || targetValue > max) {
1091
- return buildFailure('UNKNOWN', `targetValue ${targetValue} is outside the control range ${min}..${max}`, resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
1092
- }
1093
- const axis = ToolsInteract._controlAxis(currentEl, bounds);
1094
- const targetPoint = ToolsInteract._buildConservativeControlPoint(bounds, targetValue, currentValue, min, max, axis);
1095
- const currentPoint = currentValue !== null
1096
- ? ToolsInteract._buildControlPoint(bounds, (currentValue - min) / (max - min), axis)
1097
- : ToolsInteract._buildControlPoint(bounds, 0.5, axis);
1098
- const probePoints = ToolsInteract._buildAdjustmentProbePoints(bounds, targetValue, currentValue, min, max, axis);
1099
1315
  const runVerification = async () => {
1316
+ if (inferredSliderState) {
1317
+ const tree = await ToolsObserve.getUITreeHandler({ platform: resolvedPlatform, deviceId: resolvedDeviceId });
1318
+ const elements = Array.isArray(tree?.elements) ? tree.elements : [];
1319
+ const refreshed = ToolsInteract._inferLabeledSliderState(elements, currentEl);
1320
+ const observedValue = refreshed?.current ?? null;
1321
+ const observedState = observedValue !== null
1322
+ ? { property, value: observedValue, raw_value: observedValue }
1323
+ : actualState;
1324
+ return {
1325
+ verification: {
1326
+ success: observedValue !== null && Math.abs(observedValue - normalizedTargetValue) <= effectiveTolerance,
1327
+ observed_state: observedState,
1328
+ reason: observedValue !== null
1329
+ ? 'visible labeled value read from nearby slider labels'
1330
+ : 'visible labeled value unavailable'
1331
+ },
1332
+ observedState,
1333
+ withinTolerance: observedValue !== null && Math.abs(observedValue - normalizedTargetValue) <= effectiveTolerance
1334
+ };
1335
+ }
1100
1336
  const verification = await ToolsInteract.expectStateHandler({
1101
1337
  element_id: resolvedTarget?.elementId ?? element_id,
1102
1338
  selector: selector ?? undefined,
1103
1339
  property,
1104
- expected: targetValue,
1340
+ expected: normalizedTargetValue,
1105
1341
  platform: resolvedPlatform,
1106
1342
  deviceId: resolvedDeviceId
1107
1343
  });
1108
- const observedValue = typeof verification?.observed_state?.value === 'number'
1109
- ? verification.observed_state.value
1110
- : typeof verification?.observed_state?.raw_value === 'number'
1111
- ? verification.observed_state.raw_value
1112
- : null;
1344
+ const observedValue = ToolsInteract._parseNumericControlValue(verification?.observed_state?.value) ??
1345
+ ToolsInteract._parseNumericControlValue(verification?.observed_state?.raw_value);
1113
1346
  const observedState = observedValue !== null
1114
1347
  ? {
1115
1348
  property,
@@ -1120,88 +1353,266 @@ export class ToolsInteract {
1120
1353
  return {
1121
1354
  verification,
1122
1355
  observedState,
1123
- withinTolerance: observedValue !== null && Math.abs(observedValue - targetValue) <= normalizedTolerance
1356
+ withinTolerance: observedValue !== null && Math.abs(observedValue - normalizedTargetValue) <= effectiveTolerance
1124
1357
  };
1125
1358
  };
1126
1359
  let actionDevice = currentDevice;
1127
1360
  let observedState = actualState;
1128
1361
  let verification = null;
1129
1362
  let verificationResult = { verification: null, observedState: actualState, withinTolerance: false };
1130
- for (let i = 0; i < probePoints.length; i++) {
1131
- const probePoint = probePoints[i];
1132
- lastAdjustmentMode = 'coordinate';
1133
- recordTraceStep('execute', 'retry', {
1134
- attempt: attemptCount + 1,
1135
- mode: 'coordinate',
1136
- point: probePoint
1137
- });
1138
- const actionResult = await ToolsInteract.tapHandler({
1139
- platform: resolvedPlatform,
1140
- x: probePoint.x,
1141
- y: probePoint.y,
1142
- deviceId: resolvedDeviceId
1143
- });
1144
- attemptCount++;
1145
- actionDevice = actionResult.device ?? actionDevice;
1146
- if (!actionResult.success) {
1363
+ const directActionSupported = ToolsInteract._hasDirectAdjustmentAction(currentEl);
1364
+ if (directActionSupported && attemptCount < attemptsLimit) {
1365
+ const { interact, resolved: actionResolved } = await ToolsInteract.getInteractionService(resolvedPlatform, resolvedDeviceId);
1366
+ if (typeof interact.setAdjustableValue === 'function') {
1367
+ lastAdjustmentMode = 'semantic';
1147
1368
  recordTraceStep('execute', 'retry', {
1148
- attempt: attemptCount,
1149
- mode: 'coordinate',
1150
- point: probePoint,
1151
- success: false
1369
+ attempt: attemptCount + 1,
1370
+ mode: 'semantic',
1371
+ strategy: 'direct_set_value',
1372
+ target_value: normalizedTargetValue
1152
1373
  });
1153
- continue;
1154
- }
1155
- verificationResult = await runVerification();
1156
- observedState = verificationResult.observedState;
1157
- lastObservedState = observedState;
1158
- recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
1159
- attempt: attemptCount,
1160
- property,
1161
- target_value: targetValue,
1162
- actual_state: observedState,
1163
- reason: verificationResult.verification?.reason ?? 'control did not converge yet'
1164
- });
1165
- if (verificationResult.withinTolerance) {
1166
- const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
1167
- const base = buildActionExecutionResult({
1168
- actionType,
1169
- sourceModule: 'interact',
1170
- device: actionDevice ?? currentDevice,
1171
- selector: targetSelector,
1172
- resolved: resolvedTarget,
1173
- success: true,
1174
- uiFingerprintBefore: fingerprintBefore,
1175
- uiFingerprintAfter,
1176
- details: {
1177
- target_value: targetValue,
1178
- tolerance: normalizedTolerance,
1374
+ const actionResult = await interact.setAdjustableValue({
1375
+ deviceId: actionResolved.id,
1376
+ target: resolvedTarget,
1377
+ property,
1378
+ value: normalizedTargetValue
1379
+ });
1380
+ attemptCount++;
1381
+ actionDevice = actionResult?.device ?? actionDevice;
1382
+ if (actionResult?.success !== false) {
1383
+ verificationResult = await runVerification();
1384
+ observedState = verificationResult.observedState;
1385
+ lastObservedState = observedState;
1386
+ recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
1387
+ attempt: attemptCount,
1179
1388
  property,
1180
- attempts: attemptCount,
1181
- adjustment_mode: lastAdjustmentMode,
1389
+ target_value: normalizedTargetValue,
1390
+ actual_state: observedState,
1391
+ reason: verificationResult.verification?.reason ?? 'direct adjustment did not converge yet'
1392
+ });
1393
+ if (verificationResult.withinTolerance) {
1394
+ const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
1395
+ const base = buildActionExecutionResult({
1396
+ actionType,
1397
+ sourceModule: 'interact',
1398
+ device: actionDevice ?? currentDevice,
1399
+ selector: targetSelector,
1400
+ resolved: resolvedTarget,
1401
+ success: true,
1402
+ uiFingerprintBefore: fingerprintBefore,
1403
+ uiFingerprintAfter,
1404
+ details: {
1405
+ target_value: normalizedTargetValue,
1406
+ tolerance: effectiveTolerance,
1407
+ property,
1408
+ attempts: attemptCount,
1409
+ adjustment_mode: lastAdjustmentMode,
1410
+ actual_state: observedState,
1411
+ converged: true,
1412
+ within_tolerance: true,
1413
+ reason: verificationResult.verification?.reason ?? 'control converged to target value'
1414
+ },
1415
+ traceSteps
1416
+ });
1417
+ return {
1418
+ ...base,
1419
+ target_state: {
1420
+ property,
1421
+ target_value: normalizedTargetValue,
1422
+ tolerance: effectiveTolerance
1423
+ },
1424
+ actual_state: observedState,
1425
+ within_tolerance: true,
1426
+ converged: true,
1427
+ attempts: attemptCount,
1428
+ adjustment_mode: lastAdjustmentMode
1429
+ };
1430
+ }
1431
+ cachedResolvedMatch = null;
1432
+ if (attemptCount < attemptsLimit) {
1433
+ continue;
1434
+ }
1435
+ }
1436
+ }
1437
+ }
1438
+ const directions = ToolsInteract._adjustmentDirections(currentEl);
1439
+ if ((directions.increment || directions.decrement) && attemptCount < attemptsLimit) {
1440
+ const { interact, resolved: actionResolved } = await ToolsInteract.getInteractionService(resolvedPlatform, resolvedDeviceId);
1441
+ const adjustByDirection = typeof interact.adjustAccessibleValue === 'function'
1442
+ ? interact.adjustAccessibleValue.bind(interact)
1443
+ : null;
1444
+ while (adjustByDirection && attemptCount < attemptsLimit) {
1445
+ const direction = normalizedTargetValue > (observedState?.value ?? currentValue) ? 'increment' : 'decrement';
1446
+ if ((direction === 'increment' && !directions.increment) || (direction === 'decrement' && !directions.decrement))
1447
+ break;
1448
+ lastAdjustmentMode = 'semantic';
1449
+ recordTraceStep('execute', 'retry', {
1450
+ attempt: attemptCount + 1,
1451
+ mode: 'semantic',
1452
+ strategy: 'accessibility_increment_decrement',
1453
+ direction
1454
+ });
1455
+ const actionResult = await adjustByDirection({
1456
+ deviceId: actionResolved.id,
1457
+ target: resolvedTarget,
1458
+ property,
1459
+ direction
1460
+ });
1461
+ attemptCount++;
1462
+ actionDevice = actionResult?.device ?? actionDevice;
1463
+ if (actionResult?.success === false)
1464
+ break;
1465
+ verificationResult = await runVerification();
1466
+ observedState = verificationResult.observedState;
1467
+ lastObservedState = observedState;
1468
+ recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
1469
+ attempt: attemptCount,
1470
+ property,
1471
+ target_value: normalizedTargetValue,
1472
+ actual_state: observedState,
1473
+ reason: verificationResult.verification?.reason ?? 'incremental adjustment did not converge yet'
1474
+ });
1475
+ if (verificationResult.withinTolerance) {
1476
+ const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
1477
+ const base = buildActionExecutionResult({
1478
+ actionType,
1479
+ sourceModule: 'interact',
1480
+ device: actionDevice ?? currentDevice,
1481
+ selector: targetSelector,
1482
+ resolved: resolvedTarget,
1483
+ success: true,
1484
+ uiFingerprintBefore: fingerprintBefore,
1485
+ uiFingerprintAfter,
1486
+ details: {
1487
+ target_value: normalizedTargetValue,
1488
+ tolerance: effectiveTolerance,
1489
+ property,
1490
+ attempts: attemptCount,
1491
+ adjustment_mode: lastAdjustmentMode,
1492
+ actual_state: observedState,
1493
+ converged: true,
1494
+ within_tolerance: true,
1495
+ reason: verificationResult.verification?.reason ?? 'control converged to target value'
1496
+ },
1497
+ traceSteps
1498
+ });
1499
+ return {
1500
+ ...base,
1501
+ target_state: {
1502
+ property,
1503
+ target_value: normalizedTargetValue,
1504
+ tolerance: effectiveTolerance
1505
+ },
1182
1506
  actual_state: observedState,
1183
- converged: true,
1184
1507
  within_tolerance: true,
1185
- reason: verificationResult.verification?.reason ?? 'control converged to target value'
1186
- },
1187
- traceSteps
1508
+ converged: true,
1509
+ attempts: attemptCount,
1510
+ adjustment_mode: lastAdjustmentMode
1511
+ };
1512
+ }
1513
+ cachedResolvedMatch = null;
1514
+ if (attemptCount < attemptsLimit) {
1515
+ continue adjustmentAttempts;
1516
+ }
1517
+ }
1518
+ }
1519
+ if (!bounds) {
1520
+ return buildFailure('ELEMENT_NOT_INTERACTABLE', 'adjustable control has no bounds', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
1521
+ }
1522
+ if (!valueRange) {
1523
+ return buildFailure('UNKNOWN', 'value_range unavailable', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
1524
+ }
1525
+ const min = valueRange.min;
1526
+ const max = valueRange.max;
1527
+ const axis = ToolsInteract._controlAxis(currentEl, bounds);
1528
+ const targetPoint = ToolsInteract._buildConservativeControlPoint(bounds, normalizedTargetValue, currentValue, min, max, axis);
1529
+ const currentPoint = ToolsInteract._buildControlPoint(bounds, (currentValue - min) / (max - min), axis);
1530
+ if (!coordinateTapsAwaitingFreshGesture) {
1531
+ const probePoints = inferredSliderState
1532
+ ? [targetPoint]
1533
+ : ToolsInteract._buildAdjustmentProbePoints(bounds, normalizedTargetValue, currentValue, min, max, axis);
1534
+ for (let i = 0; i < probePoints.length && attemptCount < attemptsLimit; i++) {
1535
+ const probePoint = probePoints[i];
1536
+ lastAdjustmentMode = 'coordinate';
1537
+ recordTraceStep('execute', 'retry', {
1538
+ attempt: attemptCount + 1,
1539
+ mode: 'coordinate',
1540
+ point: probePoint
1188
1541
  });
1189
- return {
1190
- ...base,
1191
- target_state: {
1192
- property,
1193
- target_value: targetValue,
1194
- tolerance: normalizedTolerance
1195
- },
1542
+ const actionResult = await ToolsInteract.tapHandler({
1543
+ platform: resolvedPlatform,
1544
+ x: probePoint.x,
1545
+ y: probePoint.y,
1546
+ deviceId: resolvedDeviceId
1547
+ });
1548
+ attemptCount++;
1549
+ actionDevice = actionResult.device ?? actionDevice;
1550
+ if (!actionResult.success) {
1551
+ recordTraceStep('execute', 'retry', {
1552
+ attempt: attemptCount,
1553
+ mode: 'coordinate',
1554
+ point: probePoint,
1555
+ success: false
1556
+ });
1557
+ continue;
1558
+ }
1559
+ verificationResult = await runVerification();
1560
+ observedState = verificationResult.observedState;
1561
+ lastObservedState = observedState;
1562
+ recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
1563
+ attempt: attemptCount,
1564
+ property,
1565
+ target_value: normalizedTargetValue,
1196
1566
  actual_state: observedState,
1197
- within_tolerance: true,
1198
- converged: true,
1199
- attempts: attemptCount,
1200
- adjustment_mode: lastAdjustmentMode
1201
- };
1567
+ reason: verificationResult.verification?.reason ?? 'control did not converge yet'
1568
+ });
1569
+ if (verificationResult.withinTolerance) {
1570
+ const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
1571
+ const base = buildActionExecutionResult({
1572
+ actionType,
1573
+ sourceModule: 'interact',
1574
+ device: actionDevice ?? currentDevice,
1575
+ selector: targetSelector,
1576
+ resolved: resolvedTarget,
1577
+ success: true,
1578
+ uiFingerprintBefore: fingerprintBefore,
1579
+ uiFingerprintAfter,
1580
+ details: {
1581
+ target_value: normalizedTargetValue,
1582
+ tolerance: effectiveTolerance,
1583
+ property,
1584
+ attempts: attemptCount,
1585
+ adjustment_mode: lastAdjustmentMode,
1586
+ actual_state: observedState,
1587
+ converged: true,
1588
+ within_tolerance: true,
1589
+ reason: verificationResult.verification?.reason ?? 'control converged to target value'
1590
+ },
1591
+ traceSteps
1592
+ });
1593
+ return {
1594
+ ...base,
1595
+ target_state: {
1596
+ property,
1597
+ target_value: normalizedTargetValue,
1598
+ tolerance: effectiveTolerance
1599
+ },
1600
+ actual_state: observedState,
1601
+ within_tolerance: true,
1602
+ converged: true,
1603
+ attempts: attemptCount,
1604
+ adjustment_mode: lastAdjustmentMode
1605
+ };
1606
+ }
1607
+ coordinateTapsAwaitingFreshGesture = true;
1608
+ cachedResolvedMatch = null;
1609
+ if (attemptCount < attemptsLimit) {
1610
+ continue adjustmentAttempts;
1611
+ }
1202
1612
  }
1203
1613
  }
1204
- if (currentValue !== null) {
1614
+ if (attemptCount < attemptsLimit) {
1615
+ coordinateTapsAwaitingFreshGesture = false;
1205
1616
  lastAdjustmentMode = 'gesture';
1206
1617
  recordTraceStep('execute', 'retry', {
1207
1618
  attempt: attemptCount + 1,
@@ -1215,7 +1626,7 @@ export class ToolsInteract {
1215
1626
  y1: currentPoint.y,
1216
1627
  x2: targetPoint.x,
1217
1628
  y2: targetPoint.y,
1218
- duration: 220,
1629
+ duration: inferredSliderState ? 520 : 220,
1219
1630
  deviceId: resolvedDeviceId
1220
1631
  });
1221
1632
  attemptCount++;
@@ -1236,7 +1647,7 @@ export class ToolsInteract {
1236
1647
  recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
1237
1648
  attempt: attemptCount,
1238
1649
  property,
1239
- target_value: targetValue,
1650
+ target_value: normalizedTargetValue,
1240
1651
  actual_state: observedState,
1241
1652
  reason: verificationResult.verification?.reason ?? 'gesture adjustment did not converge yet'
1242
1653
  });
@@ -1252,8 +1663,8 @@ export class ToolsInteract {
1252
1663
  uiFingerprintBefore: fingerprintBefore,
1253
1664
  uiFingerprintAfter,
1254
1665
  details: {
1255
- target_value: targetValue,
1256
- tolerance: normalizedTolerance,
1666
+ target_value: normalizedTargetValue,
1667
+ tolerance: effectiveTolerance,
1257
1668
  property,
1258
1669
  attempts: attemptCount,
1259
1670
  adjustment_mode: lastAdjustmentMode,
@@ -1268,8 +1679,8 @@ export class ToolsInteract {
1268
1679
  ...base,
1269
1680
  target_state: {
1270
1681
  property,
1271
- target_value: targetValue,
1272
- tolerance: normalizedTolerance
1682
+ target_value: normalizedTargetValue,
1683
+ tolerance: effectiveTolerance
1273
1684
  },
1274
1685
  actual_state: observedState,
1275
1686
  within_tolerance: true,
@@ -1285,7 +1696,7 @@ export class ToolsInteract {
1285
1696
  recordTraceStep('verify', 'success', {
1286
1697
  attempt: attemptCount,
1287
1698
  property,
1288
- target_value: targetValue,
1699
+ target_value: normalizedTargetValue,
1289
1700
  actual_state: observedState,
1290
1701
  reason: verification?.reason ?? 'control converged to target value'
1291
1702
  });
@@ -1300,8 +1711,8 @@ export class ToolsInteract {
1300
1711
  uiFingerprintBefore: fingerprintBefore,
1301
1712
  uiFingerprintAfter,
1302
1713
  details: {
1303
- target_value: targetValue,
1304
- tolerance: normalizedTolerance,
1714
+ target_value: normalizedTargetValue,
1715
+ tolerance: effectiveTolerance,
1305
1716
  property,
1306
1717
  attempts: attemptCount,
1307
1718
  adjustment_mode: lastAdjustmentMode,
@@ -1316,8 +1727,8 @@ export class ToolsInteract {
1316
1727
  ...base,
1317
1728
  target_state: {
1318
1729
  property,
1319
- target_value: targetValue,
1320
- tolerance: normalizedTolerance
1730
+ target_value: normalizedTargetValue,
1731
+ tolerance: effectiveTolerance
1321
1732
  },
1322
1733
  actual_state: observedState,
1323
1734
  within_tolerance: true,
@@ -1326,22 +1737,24 @@ export class ToolsInteract {
1326
1737
  adjustment_mode: lastAdjustmentMode
1327
1738
  };
1328
1739
  }
1329
- cachedResolvedMatch = {
1330
- el: {
1331
- ...currentEl,
1332
- state: {
1333
- ...(currentEl.state ?? null),
1334
- ...(observedState ? {
1335
- [observedState.property]: observedState.value,
1336
- raw_value: observedState.raw_value ?? observedState.value
1337
- } : {})
1338
- }
1339
- },
1340
- idx: resolved.match.idx
1341
- };
1740
+ cachedResolvedMatch = inferredSliderState
1741
+ ? null
1742
+ : {
1743
+ el: {
1744
+ ...currentEl,
1745
+ state: {
1746
+ ...(currentEl.state ?? null),
1747
+ ...(observedState ? {
1748
+ [observedState.property]: observedState.value,
1749
+ raw_value: observedState.raw_value ?? observedState.value
1750
+ } : {})
1751
+ }
1752
+ },
1753
+ idx: resolved.match.idx
1754
+ };
1342
1755
  }
1343
1756
  const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
1344
- return buildFailure('TIMEOUT', 'control did not converge within the allotted attempts', resolvedTarget, currentDevice, lastObservedState, attemptCount, lastAdjustmentMode, true, uiFingerprintAfter);
1757
+ return buildFailure('CONTROL_CONVERGENCE_FAILED', 'control did not converge within the allotted attempts', resolvedTarget, currentDevice, lastObservedState, attemptCount, lastAdjustmentMode, true, uiFingerprintAfter);
1345
1758
  }
1346
1759
  static async swipeHandler({ platform = 'android', x1, y1, x2, y2, duration, deviceId }) {
1347
1760
  const { interact, resolved } = await ToolsInteract.getInteractionService(platform, deviceId);