mobile-debug-mcp 0.30.0 → 0.30.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/interact/index.js +876 -155
- package/dist/observe/index.js +1 -0
- package/dist/observe/snapshot-metadata.js +62 -1
- package/dist/server/tool-definitions.js +20 -11
- package/dist/server/tool-handlers.js +7 -5
- package/dist/server-core.js +1 -1
- package/docs/CHANGELOG.md +9 -0
- package/docs/ROADMAP.md +85 -209
- package/docs/{rfcs → specs-deltas}/013-wait-and-synchronization-reliability.md +15 -35
- package/docs/{rfcs → specs-deltas}/014-actionability-resolution.md +32 -30
- package/docs/specs-deltas/015-adjustable-control-support.md +541 -0
- package/package.json +1 -1
- package/skills/spec-review/SKILL.md +52 -0
- package/skills/spec-review/references/spec-review-checklist.md +13 -0
- package/skills/spec-review/references/spec-review-template.md +28 -0
- package/src/interact/index.ts +1008 -171
- package/src/observe/index.ts +1 -0
- package/src/observe/snapshot-metadata.ts +69 -2
- package/src/server/tool-definitions.ts +20 -11
- package/src/server/tool-handlers.ts +7 -5
- package/src/server-core.ts +1 -1
- package/src/types.ts +26 -0
- package/test/unit/interact/adjust_control.test.ts +690 -33
- package/test/unit/interact/subtree_collection.test.ts +24 -0
- package/test/unit/interact/tap_element.test.ts +71 -0
- package/test/unit/interact/wait_for_ui_change.test.ts +92 -1
- package/test/unit/observe/snapshot_metadata.test.ts +67 -0
- package/test/unit/server/contract.test.ts +4 -0
- package/skills/rfc-review/SKILL.md +0 -52
- package/skills/rfc-review/references/rfc-review-checklist.md +0 -12
- package/skills/rfc-review/references/rfc-review-template.md +0 -28
- /package/docs/{rfcs → specs-deltas}/001-state-verification.md +0 -0
- /package/docs/{rfcs → specs-deltas}/002-richer-element-identity +0 -0
- /package/docs/{rfcs → specs-deltas}/003-wait-and-synchronization-reliability.md +0 -0
- /package/docs/{rfcs → specs-deltas}/004-action-verification-routing.md +0 -0
- /package/docs/{rfcs → specs-deltas}/005-unified-action-execution-and-verification-model.md +0 -0
- /package/docs/{rfcs → specs-deltas}/006-runtime-action-instrumentation-and-binding-layer.md +0 -0
- /package/docs/{rfcs → specs-deltas}/007-actionability-resolution-and-executable-target-selection.md +0 -0
- /package/docs/{rfcs → specs-deltas}/008-adjustable-control-support-and-semantic-value-manipulation.md +0 -0
- /package/docs/{rfcs → specs-deltas}/009-semantic-control-modeling-for-custom-and-composite-controls.md +0 -0
- /package/docs/{rfcs → specs-deltas}/010-verification-stabilization-and-temporal-convergence.md +0 -0
- /package/docs/{rfcs → specs-deltas}/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows.md +0 -0
- /package/docs/{rfcs → specs-deltas}/011.1-recovery-contract-types-and-runtime-wiring-spec.md +0 -0
- /package/docs/{rfcs → specs-deltas}/012-action-trace-and-xecution-observability.md +0 -0
package/src/interact/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ interface UiElement {
|
|
|
43
43
|
focusable?: boolean
|
|
44
44
|
visible?: boolean
|
|
45
45
|
parentId?: number | string | null
|
|
46
|
+
children?: number[]
|
|
46
47
|
_index?: number
|
|
47
48
|
_interactable?: boolean
|
|
48
49
|
_sliderLike?: boolean
|
|
@@ -52,6 +53,10 @@ interface UiElement {
|
|
|
52
53
|
test_tag?: string | null
|
|
53
54
|
selector?: { value: string | null, confidence: { score: number, reason: string } | null } | null
|
|
54
55
|
semantic?: UIElementSemanticMetadata | null
|
|
56
|
+
actions?: string[] | null
|
|
57
|
+
accessibilityActions?: string[] | null
|
|
58
|
+
runtimeActions?: string[] | null
|
|
59
|
+
platformActions?: string[] | null
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
interface ResolvedUiElementContext {
|
|
@@ -60,6 +65,7 @@ interface ResolvedUiElementContext {
|
|
|
60
65
|
deviceId?: string
|
|
61
66
|
bounds: [number, number, number, number] | null
|
|
62
67
|
index: number
|
|
68
|
+
stable_id?: string | null
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
interface UiResolution {
|
|
@@ -73,6 +79,24 @@ interface UiChangeSignatureSet {
|
|
|
73
79
|
state: string | null
|
|
74
80
|
}
|
|
75
81
|
|
|
82
|
+
interface UiChangeScopeResolution {
|
|
83
|
+
scope: 'screen' | 'subtree'
|
|
84
|
+
target: string | null
|
|
85
|
+
resolved: boolean
|
|
86
|
+
resolvedIndex: number | null
|
|
87
|
+
resolvedStableId: string | null
|
|
88
|
+
reason: string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface UiChangeScopeResult {
|
|
92
|
+
elements: UiElement[]
|
|
93
|
+
resolution: UiChangeScopeResolution
|
|
94
|
+
error?: {
|
|
95
|
+
code: 'INVALID_SCOPE' | 'ELEMENT_NOT_FOUND'
|
|
96
|
+
message: string
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
76
100
|
interface RankedResolutionCandidate {
|
|
77
101
|
el: UiElement
|
|
78
102
|
idx: number
|
|
@@ -81,6 +105,15 @@ interface RankedResolutionCandidate {
|
|
|
81
105
|
interactable: boolean
|
|
82
106
|
}
|
|
83
107
|
|
|
108
|
+
interface InferredLabeledSliderState {
|
|
109
|
+
current: number
|
|
110
|
+
min: number
|
|
111
|
+
max: number
|
|
112
|
+
value_range: { min: number; max: number }
|
|
113
|
+
touch_bounds: [number, number, number, number]
|
|
114
|
+
source: 'nearby_labels'
|
|
115
|
+
}
|
|
116
|
+
|
|
84
117
|
function buildObservationTrace({
|
|
85
118
|
actionType,
|
|
86
119
|
stage,
|
|
@@ -201,11 +234,88 @@ export class ToolsInteract {
|
|
|
201
234
|
return null
|
|
202
235
|
}
|
|
203
236
|
|
|
237
|
+
private static _resolveParentIndex(elements: UiElement[], parentId: number | string | null | undefined): number | null {
|
|
238
|
+
if (parentId === undefined || parentId === null) return null
|
|
239
|
+
|
|
240
|
+
if (typeof parentId === 'number' && Number.isInteger(parentId) && parentId >= 0 && parentId < elements.length) {
|
|
241
|
+
return parentId
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (typeof parentId === 'string') {
|
|
245
|
+
const normalized = ToolsInteract._normalize(parentId)
|
|
246
|
+
if (!normalized) return null
|
|
247
|
+
|
|
248
|
+
if (/^\d+$/.test(normalized)) {
|
|
249
|
+
const index = Number(normalized)
|
|
250
|
+
if (index >= 0 && index < elements.length) return index
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const foundIndex = elements.findIndex((el) => {
|
|
254
|
+
if (!el) return false
|
|
255
|
+
return ToolsInteract._normalize(el.resourceId ?? el.resourceID ?? el.id ?? '') === normalized ||
|
|
256
|
+
ToolsInteract._normalize(el.stable_id ?? '') === normalized
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
return foundIndex >= 0 ? foundIndex : null
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return null
|
|
263
|
+
}
|
|
264
|
+
|
|
204
265
|
private static _isVisibleElement(el: UiElement): boolean {
|
|
205
266
|
const bounds = ToolsInteract._normalizeBounds(el.bounds)
|
|
206
267
|
return !!el.visible && !!bounds && bounds[2] > bounds[0] && bounds[3] > bounds[1]
|
|
207
268
|
}
|
|
208
269
|
|
|
270
|
+
private static _isTapActionable(
|
|
271
|
+
el: UiElement,
|
|
272
|
+
storedStableId?: string | null,
|
|
273
|
+
platform?: 'android' | 'ios'
|
|
274
|
+
): { actionable: boolean, failureCode?: ActionFailureCode, reason?: string } {
|
|
275
|
+
if (!ToolsInteract._isVisibleElement(el)) {
|
|
276
|
+
return { actionable: false, failureCode: 'ELEMENT_NOT_INTERACTABLE', reason: 'element is not visible' }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (el.enabled === false) {
|
|
280
|
+
return { actionable: false, failureCode: 'ELEMENT_NOT_INTERACTABLE', reason: 'element is disabled' }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const semanticTapActionable = !!el.semantic && (
|
|
284
|
+
el.semantic.is_clickable ||
|
|
285
|
+
(Array.isArray(el.semantic.supported_actions) && el.semantic.supported_actions.some((action) => ToolsInteract._normalize(action) === 'tap'))
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if (!el.clickable && !(platform === 'ios' && semanticTapActionable)) {
|
|
289
|
+
return { actionable: false, failureCode: 'ELEMENT_NOT_INTERACTABLE', reason: 'element is not clickable' }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (storedStableId) {
|
|
293
|
+
if (!el.stable_id || el.stable_id !== storedStableId) {
|
|
294
|
+
return { actionable: false, failureCode: 'STALE_REFERENCE', reason: 'element stable_id changed' }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return { actionable: true }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private static _isAdjustableActionable(el: UiElement, storedStableId?: string | null): { actionable: boolean, failureCode?: ActionFailureCode, reason?: string } {
|
|
302
|
+
if (!ToolsInteract._isVisibleElement(el)) {
|
|
303
|
+
return { actionable: false, failureCode: 'ELEMENT_NOT_INTERACTABLE', reason: 'element is not visible' }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (el.enabled === false) {
|
|
307
|
+
return { actionable: false, failureCode: 'ELEMENT_NOT_INTERACTABLE', reason: 'element is disabled' }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (storedStableId) {
|
|
311
|
+
if (!el.stable_id || el.stable_id !== storedStableId) {
|
|
312
|
+
return { actionable: false, failureCode: 'STALE_REFERENCE', reason: 'element stable_id changed' }
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return { actionable: true }
|
|
317
|
+
}
|
|
318
|
+
|
|
209
319
|
private static _computeElementId(platform: 'android' | 'ios', deviceId: string | undefined, el: UiElement, index: number): string {
|
|
210
320
|
const identity = {
|
|
211
321
|
platform,
|
|
@@ -229,7 +339,8 @@ export class ToolsInteract {
|
|
|
229
339
|
platform,
|
|
230
340
|
deviceId,
|
|
231
341
|
bounds,
|
|
232
|
-
index
|
|
342
|
+
index,
|
|
343
|
+
stable_id: el.stable_id ?? null
|
|
233
344
|
})
|
|
234
345
|
|
|
235
346
|
return {
|
|
@@ -249,6 +360,235 @@ export class ToolsInteract {
|
|
|
249
360
|
}
|
|
250
361
|
}
|
|
251
362
|
|
|
363
|
+
private static _resolveUiChangeScope(
|
|
364
|
+
tree: any,
|
|
365
|
+
scope: 'screen' | 'subtree' | undefined,
|
|
366
|
+
target: string | null | undefined
|
|
367
|
+
): UiChangeScopeResult {
|
|
368
|
+
const elements = Array.isArray(tree?.elements) ? tree.elements as UiElement[] : []
|
|
369
|
+
const normalizedScope = scope === 'subtree' ? 'subtree' : 'screen'
|
|
370
|
+
|
|
371
|
+
if (normalizedScope === 'screen') {
|
|
372
|
+
return {
|
|
373
|
+
elements,
|
|
374
|
+
resolution: {
|
|
375
|
+
scope: 'screen',
|
|
376
|
+
target: null,
|
|
377
|
+
resolved: true,
|
|
378
|
+
resolvedIndex: null,
|
|
379
|
+
resolvedStableId: null,
|
|
380
|
+
reason: 'screen scope'
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const requestedTarget = typeof target === 'string' && target.trim().length > 0 ? target.trim() : null
|
|
386
|
+
if (!requestedTarget) {
|
|
387
|
+
return {
|
|
388
|
+
elements: [],
|
|
389
|
+
resolution: {
|
|
390
|
+
scope: 'subtree',
|
|
391
|
+
target: null,
|
|
392
|
+
resolved: false,
|
|
393
|
+
resolvedIndex: null,
|
|
394
|
+
resolvedStableId: null,
|
|
395
|
+
reason: 'subtree scope requires a target element id'
|
|
396
|
+
},
|
|
397
|
+
error: {
|
|
398
|
+
code: 'INVALID_SCOPE',
|
|
399
|
+
message: 'scope=subtree requires a target element_id'
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const resolved = ToolsInteract._findScopedElement(tree, requestedTarget)
|
|
405
|
+
if (!resolved) {
|
|
406
|
+
return {
|
|
407
|
+
elements: [],
|
|
408
|
+
resolution: {
|
|
409
|
+
scope: 'subtree',
|
|
410
|
+
target: requestedTarget,
|
|
411
|
+
resolved: false,
|
|
412
|
+
resolvedIndex: null,
|
|
413
|
+
resolvedStableId: null,
|
|
414
|
+
reason: 'target element could not be resolved'
|
|
415
|
+
},
|
|
416
|
+
error: {
|
|
417
|
+
code: 'ELEMENT_NOT_FOUND',
|
|
418
|
+
message: `Target element ${requestedTarget} could not be resolved for subtree scope`
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const subtreeIndices = ToolsInteract._collectSubtreeIndices(elements, resolved.index)
|
|
424
|
+
const scopedElements = subtreeIndices.map((index) => elements[index]).filter((element): element is UiElement => !!element)
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
elements: scopedElements,
|
|
428
|
+
resolution: {
|
|
429
|
+
scope: 'subtree',
|
|
430
|
+
target: requestedTarget,
|
|
431
|
+
resolved: true,
|
|
432
|
+
resolvedIndex: resolved.index,
|
|
433
|
+
resolvedStableId: resolved.stableId,
|
|
434
|
+
reason: resolved.reason
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private static _findScopedElement(tree: any, targetElementId: string): { index: number, stableId: string | null, reason: string } | null {
|
|
440
|
+
const elements = Array.isArray(tree?.elements) ? tree.elements as UiElement[] : []
|
|
441
|
+
const platform = tree?.device?.platform === 'ios' ? 'ios' : 'android'
|
|
442
|
+
const deviceId = tree?.device?.id ?? undefined
|
|
443
|
+
const normalizedTarget = ToolsInteract._normalize(targetElementId)
|
|
444
|
+
|
|
445
|
+
for (let i = 0; i < elements.length; i++) {
|
|
446
|
+
const el = elements[i]
|
|
447
|
+
if (!el) continue
|
|
448
|
+
|
|
449
|
+
const computedElementId = ToolsInteract._computeElementId(platform, deviceId, el, i)
|
|
450
|
+
if (computedElementId === targetElementId) {
|
|
451
|
+
return {
|
|
452
|
+
index: i,
|
|
453
|
+
stableId: el.stable_id ?? null,
|
|
454
|
+
reason: 'element_id_match'
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
for (let i = 0; i < elements.length; i++) {
|
|
460
|
+
const el = elements[i]
|
|
461
|
+
if (!el) continue
|
|
462
|
+
|
|
463
|
+
if (el.stable_id && ToolsInteract._normalize(el.stable_id) === normalizedTarget) {
|
|
464
|
+
return {
|
|
465
|
+
index: i,
|
|
466
|
+
stableId: el.stable_id,
|
|
467
|
+
reason: 'stable_id_match'
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const storedContext = ToolsInteract._resolvedUiElements.get(targetElementId)
|
|
473
|
+
if (storedContext?.stable_id) {
|
|
474
|
+
const normalizedStoredStableId = ToolsInteract._normalize(storedContext.stable_id)
|
|
475
|
+
for (let i = 0; i < elements.length; i++) {
|
|
476
|
+
const el = elements[i]
|
|
477
|
+
if (!el?.stable_id) continue
|
|
478
|
+
if (ToolsInteract._normalize(el.stable_id) === normalizedStoredStableId) {
|
|
479
|
+
return {
|
|
480
|
+
index: i,
|
|
481
|
+
stableId: el.stable_id,
|
|
482
|
+
reason: 'stored_stable_id_match'
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return null
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private static _collectSubtreeIndices(elements: UiElement[], rootIndex: number): number[] {
|
|
492
|
+
if (!Array.isArray(elements) || rootIndex < 0 || rootIndex >= elements.length) return []
|
|
493
|
+
|
|
494
|
+
const visited = new Set<number>()
|
|
495
|
+
const stack = [rootIndex]
|
|
496
|
+
const result: number[] = []
|
|
497
|
+
|
|
498
|
+
while (stack.length > 0) {
|
|
499
|
+
const index = stack.pop()
|
|
500
|
+
if (index === undefined || visited.has(index) || index < 0 || index >= elements.length) continue
|
|
501
|
+
visited.add(index)
|
|
502
|
+
result.push(index)
|
|
503
|
+
|
|
504
|
+
const element = elements[index]
|
|
505
|
+
if (!element) continue
|
|
506
|
+
|
|
507
|
+
const directChildren = new Set<number>()
|
|
508
|
+
if (Array.isArray(element.children)) {
|
|
509
|
+
for (const childIndex of element.children) {
|
|
510
|
+
if (typeof childIndex === 'number' && Number.isInteger(childIndex) && childIndex >= 0 && childIndex < elements.length) {
|
|
511
|
+
directChildren.add(childIndex)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
for (let i = 0; i < elements.length; i++) {
|
|
517
|
+
if (ToolsInteract._resolveParentIndex(elements, elements[i]?.parentId) === index) {
|
|
518
|
+
directChildren.add(i)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
for (const childIndex of directChildren) {
|
|
523
|
+
if (!visited.has(childIndex)) stack.push(childIndex)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return result.sort((left, right) => left - right)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private static _changeIdentityForElement(el: UiElement, index: number): string {
|
|
531
|
+
const stableId = ToolsInteract._normalize(el.stable_id)
|
|
532
|
+
if (stableId) return `stable:${stableId}`
|
|
533
|
+
|
|
534
|
+
return `fallback:${ToolsInteract._hash({
|
|
535
|
+
text: ToolsInteract._normalize(el.text ?? el.label ?? el.value ?? ''),
|
|
536
|
+
contentDescription: ToolsInteract._normalize(el.contentDescription ?? el.contentDesc ?? el.accessibilityLabel ?? ''),
|
|
537
|
+
resourceId: ToolsInteract._normalize(el.resourceId ?? el.resourceID ?? el.id ?? ''),
|
|
538
|
+
type: ToolsInteract._normalize(el.type ?? el.class ?? ''),
|
|
539
|
+
bounds: ToolsInteract._normalizeBounds(el.bounds) ?? [0, 0, 0, 0],
|
|
540
|
+
index
|
|
541
|
+
})}`
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private static _summarizeUiChangeDelta(initialElements: UiElement[], currentElements: UiElement[]) {
|
|
545
|
+
const buildMap = (elements: UiElement[]) => {
|
|
546
|
+
const map = new Map<string, string>()
|
|
547
|
+
for (let i = 0; i < elements.length; i++) {
|
|
548
|
+
const element = elements[i]
|
|
549
|
+
if (!element) continue
|
|
550
|
+
const key = ToolsInteract._changeIdentityForElement(element, i)
|
|
551
|
+
map.set(key, ToolsInteract._hash({
|
|
552
|
+
text: ToolsInteract._normalize(element.text ?? element.label ?? element.value ?? ''),
|
|
553
|
+
contentDescription: ToolsInteract._normalize(element.contentDescription ?? element.contentDesc ?? element.accessibilityLabel ?? ''),
|
|
554
|
+
resourceId: ToolsInteract._normalize(element.resourceId ?? element.resourceID ?? element.id ?? ''),
|
|
555
|
+
type: ToolsInteract._normalize(element.type ?? element.class ?? ''),
|
|
556
|
+
bounds: ToolsInteract._normalizeBounds(element.bounds) ?? [0, 0, 0, 0],
|
|
557
|
+
state: element.state ?? null,
|
|
558
|
+
visible: !!element.visible,
|
|
559
|
+
enabled: !!element.enabled,
|
|
560
|
+
clickable: !!element.clickable
|
|
561
|
+
}))
|
|
562
|
+
}
|
|
563
|
+
return map
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const initialMap = buildMap(initialElements)
|
|
567
|
+
const currentMap = buildMap(currentElements)
|
|
568
|
+
let added = 0
|
|
569
|
+
let removed = 0
|
|
570
|
+
let mutated = 0
|
|
571
|
+
|
|
572
|
+
for (const [key, value] of currentMap.entries()) {
|
|
573
|
+
if (!initialMap.has(key)) {
|
|
574
|
+
added++
|
|
575
|
+
} else if (initialMap.get(key) !== value) {
|
|
576
|
+
mutated++
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
for (const key of initialMap.keys()) {
|
|
581
|
+
if (!currentMap.has(key)) removed++
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
total_elements: currentElements.length,
|
|
586
|
+
added_elements: added,
|
|
587
|
+
removed_elements: removed,
|
|
588
|
+
mutated_elements: mutated
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
252
592
|
private static _rememberResolvedElement(elementId: string, context: ResolvedUiElementContext) {
|
|
253
593
|
if (ToolsInteract._resolvedUiElements.has(elementId)) {
|
|
254
594
|
ToolsInteract._resolvedUiElements.delete(elementId)
|
|
@@ -392,13 +732,217 @@ export class ToolsInteract {
|
|
|
392
732
|
if (!el?.state) return null
|
|
393
733
|
const stateValue = el.state[property as keyof UIElementState]
|
|
394
734
|
if (typeof stateValue === 'number' && Number.isFinite(stateValue)) return stateValue
|
|
735
|
+
const parsedStateValue = ToolsInteract._parseNumericControlValue(stateValue)
|
|
736
|
+
if (parsedStateValue !== null) return parsedStateValue
|
|
395
737
|
if (property === 'value' || property === 'raw_value') {
|
|
396
738
|
const fallback = el.state.raw_value ?? el.state.value
|
|
397
739
|
if (typeof fallback === 'number' && Number.isFinite(fallback)) return fallback
|
|
740
|
+
return ToolsInteract._parseNumericControlValue(fallback)
|
|
741
|
+
}
|
|
742
|
+
return null
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private static _parseNumericControlValue(value: unknown): number | null {
|
|
746
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
747
|
+
if (typeof value !== 'string') return null
|
|
748
|
+
const trimmed = value.trim()
|
|
749
|
+
if (!trimmed) return null
|
|
750
|
+
|
|
751
|
+
const percent = trimmed.match(/^([-+]?\d+(?:\.\d+)?)\s*%$/)
|
|
752
|
+
if (percent) return Number(percent[1])
|
|
753
|
+
|
|
754
|
+
const fraction = trimmed.match(/^([-+]?\d+(?:\.\d+)?)\s+of\s+[-+]?\d+(?:\.\d+)?$/i)
|
|
755
|
+
if (fraction) return Number(fraction[1])
|
|
756
|
+
|
|
757
|
+
const plain = Number(trimmed)
|
|
758
|
+
if (Number.isFinite(plain)) return plain
|
|
759
|
+
|
|
760
|
+
const trailing = trimmed.match(/,\s*([-+]?\d+(?:\.\d+)?)$/)
|
|
761
|
+
if (trailing) return Number(trailing[1])
|
|
762
|
+
|
|
763
|
+
return null
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
private static _readNumericControlRange(el: UiElement | null): { min: number; max: number } | null {
|
|
767
|
+
const min = typeof el?.state?.value_range?.min === 'number' ? el.state.value_range.min : null
|
|
768
|
+
const max = typeof el?.state?.value_range?.max === 'number' ? el.state.value_range.max : null
|
|
769
|
+
if (min !== null && max !== null && Number.isFinite(min) && Number.isFinite(max) && max > min) {
|
|
770
|
+
return { min, max }
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const value = el?.state?.value ?? el?.state?.raw_value
|
|
774
|
+
if (typeof value === 'string') {
|
|
775
|
+
const fraction = value.trim().match(/^[-+]?\d+(?:\.\d+)?\s+of\s+([-+]?\d+(?:\.\d+)?)$/i)
|
|
776
|
+
if (fraction) {
|
|
777
|
+
const parsedMax = Number(fraction[1])
|
|
778
|
+
if (Number.isFinite(parsedMax) && parsedMax > 0) return { min: 0, max: parsedMax }
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return null
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
private static _readControlStep(el: UiElement | null): number | null {
|
|
786
|
+
const candidates = [
|
|
787
|
+
el?.state?.step,
|
|
788
|
+
el?.state?.value_range?.step
|
|
789
|
+
]
|
|
790
|
+
for (const candidate of candidates) {
|
|
791
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0) return candidate
|
|
398
792
|
}
|
|
399
793
|
return null
|
|
400
794
|
}
|
|
401
795
|
|
|
796
|
+
private static _quantizeTargetValue(targetValue: number, min: number, max: number, step: number | null): number {
|
|
797
|
+
if (!step) return targetValue
|
|
798
|
+
const quantized = min + (Math.round((targetValue - min) / step) * step)
|
|
799
|
+
const bounded = Math.max(min, Math.min(max, quantized))
|
|
800
|
+
const precision = ToolsInteract._decimalPrecisionForStep(step)
|
|
801
|
+
return Number(bounded.toFixed(precision))
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
private static _decimalPrecisionForStep(step: number): number {
|
|
805
|
+
const normalized = String(step).toLowerCase()
|
|
806
|
+
const [coefficient, exponentPart] = normalized.split('e')
|
|
807
|
+
const coefficientDecimals = coefficient.includes('.')
|
|
808
|
+
? coefficient.split('.')[1]?.length ?? 0
|
|
809
|
+
: 0
|
|
810
|
+
const exponent = exponentPart !== undefined ? Number(exponentPart) : 0
|
|
811
|
+
const precision = exponent < 0
|
|
812
|
+
? coefficientDecimals + Math.abs(exponent)
|
|
813
|
+
: Math.max(0, coefficientDecimals - exponent)
|
|
814
|
+
return Math.max(0, Math.min(12, precision))
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
private static _defaultAdjustTolerance(range: { min: number; max: number } | null, step: number | null): number {
|
|
818
|
+
if (step) return 0
|
|
819
|
+
if (!range) return 0
|
|
820
|
+
return Math.max(0, 0.01 * Math.abs(range.max - range.min))
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
private static _runtimeActionSet(el: UiElement | null): Set<string> {
|
|
824
|
+
const values = [
|
|
825
|
+
...(Array.isArray(el?.actions) ? el!.actions! : []),
|
|
826
|
+
...(Array.isArray(el?.accessibilityActions) ? el!.accessibilityActions! : []),
|
|
827
|
+
...(Array.isArray(el?.runtimeActions) ? el!.runtimeActions! : []),
|
|
828
|
+
...(Array.isArray(el?.platformActions) ? el!.platformActions! : [])
|
|
829
|
+
]
|
|
830
|
+
return new Set(values.map((value) => ToolsInteract._normalize(value)).filter(Boolean))
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
private static _hasDirectAdjustmentAction(el: UiElement | null): boolean {
|
|
834
|
+
const actions = ToolsInteract._runtimeActionSet(el)
|
|
835
|
+
return [
|
|
836
|
+
'action_set_progress',
|
|
837
|
+
'set_progress',
|
|
838
|
+
'setprogress',
|
|
839
|
+
'semanticsactions.setprogress',
|
|
840
|
+
'set_value',
|
|
841
|
+
'setvalue',
|
|
842
|
+
'direct_set_value'
|
|
843
|
+
].some((action) => actions.has(action))
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
private static _adjustmentDirections(el: UiElement | null): { increment: boolean; decrement: boolean } {
|
|
847
|
+
const actions = ToolsInteract._runtimeActionSet(el)
|
|
848
|
+
return {
|
|
849
|
+
increment: ['increment', 'accessibility_increment', 'action_increment'].some((action) => actions.has(action)),
|
|
850
|
+
decrement: ['decrement', 'accessibility_decrement', 'action_decrement'].some((action) => actions.has(action))
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
private static _textFromElement(el: UiElement | null): string {
|
|
855
|
+
return String(el?.text ?? el?.contentDescription ?? el?.contentDesc ?? el?.accessibilityLabel ?? el?.value ?? '').trim()
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
private static _parseLeadingNumber(text: string): number | null {
|
|
859
|
+
const match = text.trim().match(/^([-+]?\d+(?:\.\d+)?)(?:\s*[a-zA-Z%]+)?$/)
|
|
860
|
+
if (!match) return null
|
|
861
|
+
const value = Number(match[1])
|
|
862
|
+
return Number.isFinite(value) ? value : null
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
private static _inferLabeledSliderState(elements: UiElement[], track: UiElement | null): InferredLabeledSliderState | null {
|
|
866
|
+
const trackBounds = ToolsInteract._normalizeBounds(track?.bounds)
|
|
867
|
+
if (!track || !trackBounds) return null
|
|
868
|
+
const type = ToolsInteract._normalize(track.type ?? track.class ?? '')
|
|
869
|
+
const role = ToolsInteract._normalize(track.role ?? '')
|
|
870
|
+
const width = trackBounds[2] - trackBounds[0]
|
|
871
|
+
const height = trackBounds[3] - trackBounds[1]
|
|
872
|
+
const wideTrackLike = width >= 240 && height <= 220
|
|
873
|
+
const knownTrackLike = /slider|seekbar|range/.test(type) || /slider|range/.test(role)
|
|
874
|
+
if (!wideTrackLike && !knownTrackLike) return null
|
|
875
|
+
|
|
876
|
+
const trackTop = trackBounds[1]
|
|
877
|
+
const trackBottom = trackBounds[3]
|
|
878
|
+
const trackCenterX = (trackBounds[0] + trackBounds[2]) / 2
|
|
879
|
+
const labels = elements
|
|
880
|
+
.map((el, index) => {
|
|
881
|
+
const bounds = ToolsInteract._normalizeBounds(el.bounds)
|
|
882
|
+
const text = ToolsInteract._textFromElement(el)
|
|
883
|
+
const value = ToolsInteract._parseLeadingNumber(text)
|
|
884
|
+
return bounds && text && value !== null
|
|
885
|
+
? {
|
|
886
|
+
index,
|
|
887
|
+
el,
|
|
888
|
+
text,
|
|
889
|
+
value,
|
|
890
|
+
bounds,
|
|
891
|
+
centerX: (bounds[0] + bounds[2]) / 2,
|
|
892
|
+
centerY: (bounds[1] + bounds[3]) / 2,
|
|
893
|
+
hasUnit: /[a-zA-Z%]/.test(text)
|
|
894
|
+
}
|
|
895
|
+
: null
|
|
896
|
+
})
|
|
897
|
+
.filter((label): label is NonNullable<typeof label> => !!label)
|
|
898
|
+
|
|
899
|
+
const rangeLabels = labels
|
|
900
|
+
.filter((label) => {
|
|
901
|
+
const verticallyNearTrack = label.centerY >= trackTop - 520 && label.centerY <= trackBottom + 260
|
|
902
|
+
const horizontallyNearTrack = label.centerX >= trackBounds[0] - 120 && label.centerX <= trackBounds[2] + 120
|
|
903
|
+
return verticallyNearTrack && horizontallyNearTrack && label.hasUnit
|
|
904
|
+
})
|
|
905
|
+
.sort((a, b) => a.centerX - b.centerX)
|
|
906
|
+
|
|
907
|
+
if (rangeLabels.length < 2) return null
|
|
908
|
+
const minLabel = rangeLabels[0]
|
|
909
|
+
const maxLabel = rangeLabels[rangeLabels.length - 1]
|
|
910
|
+
if (!minLabel || !maxLabel || maxLabel.value <= minLabel.value) return null
|
|
911
|
+
|
|
912
|
+
const currentCandidates = labels
|
|
913
|
+
.filter((label) => {
|
|
914
|
+
if (label.index === minLabel.index || label.index === maxLabel.index) return false
|
|
915
|
+
if (label.value < minLabel.value || label.value > maxLabel.value) return false
|
|
916
|
+
if (label.centerY < trackTop - 950 || label.centerY > minLabel.centerY - 20) return false
|
|
917
|
+
return label.centerX >= trackCenterX - 80
|
|
918
|
+
})
|
|
919
|
+
.sort((a, b) => {
|
|
920
|
+
const yDistance = Math.abs((trackTop - 420) - a.centerY) - Math.abs((trackTop - 420) - b.centerY)
|
|
921
|
+
if (yDistance !== 0) return yDistance
|
|
922
|
+
return b.centerX - a.centerX
|
|
923
|
+
})
|
|
924
|
+
|
|
925
|
+
const currentLabel = currentCandidates[0]
|
|
926
|
+
if (!currentLabel) return null
|
|
927
|
+
|
|
928
|
+
const candidateBelowRangeLabels = minLabel.centerY < trackTop && maxLabel.centerY < trackTop
|
|
929
|
+
const labelDerivedBounds: [number, number, number, number] = [
|
|
930
|
+
Math.min(minLabel.bounds[0], trackBounds[0]),
|
|
931
|
+
Math.max(currentLabel.bounds[3] + 12, Math.min(minLabel.bounds[1], maxLabel.bounds[1]) - 160),
|
|
932
|
+
Math.max(maxLabel.bounds[2], trackBounds[2]),
|
|
933
|
+
Math.max(currentLabel.bounds[3] + 80, Math.min(minLabel.bounds[1], maxLabel.bounds[1]) - 32)
|
|
934
|
+
]
|
|
935
|
+
|
|
936
|
+
return {
|
|
937
|
+
current: currentLabel.value,
|
|
938
|
+
min: minLabel.value,
|
|
939
|
+
max: maxLabel.value,
|
|
940
|
+
value_range: { min: minLabel.value, max: maxLabel.value },
|
|
941
|
+
touch_bounds: candidateBelowRangeLabels ? labelDerivedBounds : trackBounds,
|
|
942
|
+
source: 'nearby_labels'
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
402
946
|
private static _buildControlPoint(bounds: [number, number, number, number], ratio: number, axis: 'horizontal' | 'vertical') {
|
|
403
947
|
const clampedRatio = Math.max(0, Math.min(1, ratio))
|
|
404
948
|
const [left, top, right, bottom] = bounds
|
|
@@ -565,20 +1109,11 @@ export class ToolsInteract {
|
|
|
565
1109
|
let safety = 0
|
|
566
1110
|
|
|
567
1111
|
while (safety < 20 && current.el && !(current.el.clickable || current.el.focusable || ToolsInteract._isSemanticActionable(current.el)) && current.el.parentId !== undefined && current.el.parentId !== null) {
|
|
568
|
-
const
|
|
569
|
-
let parentIndex: number | null = null
|
|
570
|
-
|
|
571
|
-
if (typeof parentId === 'number') parentIndex = parentId
|
|
572
|
-
else if (typeof parentId === 'string' && /^\d+$/.test(parentId)) parentIndex = Number(parentId)
|
|
1112
|
+
const parentIndex = ToolsInteract._resolveParentIndex(elements, current.el.parentId)
|
|
573
1113
|
|
|
574
1114
|
if (parentIndex !== null && elements[parentIndex]) {
|
|
575
1115
|
current = { el: elements[parentIndex], idx: parentIndex }
|
|
576
1116
|
if (current.el.clickable || current.el.focusable || ToolsInteract._isSemanticActionable(current.el)) return current
|
|
577
|
-
} else if (typeof parentId === 'string') {
|
|
578
|
-
const foundIndex = elements.findIndex((el) => el.resourceId === parentId || el.id === parentId)
|
|
579
|
-
if (foundIndex === -1) break
|
|
580
|
-
current = { el: elements[foundIndex], idx: foundIndex }
|
|
581
|
-
if (current.el.clickable || current.el.focusable || ToolsInteract._isSemanticActionable(current.el)) return current
|
|
582
1117
|
} else {
|
|
583
1118
|
break
|
|
584
1119
|
}
|
|
@@ -718,12 +1253,16 @@ export class ToolsInteract {
|
|
|
718
1253
|
|
|
719
1254
|
const resolvedTarget = ToolsInteract._resolvedTargetFromElement(resolved.elementId, currentMatch.el, currentMatch.index)
|
|
720
1255
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1256
|
+
const tapActionability = ToolsInteract._isTapActionable(currentMatch.el, resolved.stable_id, resolved.platform)
|
|
1257
|
+
if (!tapActionability.actionable) {
|
|
1258
|
+
return ToolsInteract._actionFailure(
|
|
1259
|
+
actionType,
|
|
1260
|
+
selector,
|
|
1261
|
+
resolvedTarget,
|
|
1262
|
+
tapActionability.failureCode ?? 'ELEMENT_NOT_INTERACTABLE',
|
|
1263
|
+
true,
|
|
1264
|
+
fingerprintBefore
|
|
1265
|
+
)
|
|
727
1266
|
}
|
|
728
1267
|
|
|
729
1268
|
const bounds = ToolsInteract._normalizeBounds(currentMatch.el.bounds) ?? resolved.bounds
|
|
@@ -758,8 +1297,8 @@ export class ToolsInteract {
|
|
|
758
1297
|
element_id,
|
|
759
1298
|
property = 'value',
|
|
760
1299
|
targetValue,
|
|
761
|
-
tolerance
|
|
762
|
-
maxAttempts
|
|
1300
|
+
tolerance,
|
|
1301
|
+
maxAttempts,
|
|
763
1302
|
platform,
|
|
764
1303
|
deviceId
|
|
765
1304
|
}: {
|
|
@@ -774,11 +1313,14 @@ export class ToolsInteract {
|
|
|
774
1313
|
}): Promise<AdjustControlResponse> {
|
|
775
1314
|
const actionType = 'adjust_control'
|
|
776
1315
|
const targetSelector = selector ?? (element_id ? { elementId: element_id } : null)
|
|
777
|
-
const
|
|
778
|
-
const attemptsLimit =
|
|
1316
|
+
const requestedTolerance = typeof tolerance === 'number' && Number.isFinite(tolerance) ? Math.max(0, tolerance) : null
|
|
1317
|
+
const attemptsLimit = maxAttempts === undefined || maxAttempts === null
|
|
1318
|
+
? 5
|
|
1319
|
+
: Math.max(1, Math.floor(Number(maxAttempts) || 1))
|
|
779
1320
|
const sourcePlatform: 'android' | 'ios' = platform || 'android'
|
|
780
1321
|
let resolvedPlatform = sourcePlatform
|
|
781
1322
|
let resolvedDeviceId = deviceId
|
|
1323
|
+
const storedResolvedTarget = element_id ? ToolsInteract._resolvedUiElements.get(element_id) ?? null : null
|
|
782
1324
|
const fingerprintBefore = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId)
|
|
783
1325
|
let semanticFallbackElement: FindElementResponse['element'] | null = null
|
|
784
1326
|
const traceSteps: TraceStep[] = []
|
|
@@ -825,6 +1367,7 @@ export class ToolsInteract {
|
|
|
825
1367
|
retry_depth: attempts
|
|
826
1368
|
})
|
|
827
1369
|
}
|
|
1370
|
+
const responseTolerance = requestedTolerance ?? 0
|
|
828
1371
|
const base = buildActionExecutionResult({
|
|
829
1372
|
actionType,
|
|
830
1373
|
sourceModule: 'interact',
|
|
@@ -837,7 +1380,7 @@ export class ToolsInteract {
|
|
|
837
1380
|
failure: { failureCode, retryable },
|
|
838
1381
|
details: {
|
|
839
1382
|
target_value: targetValue,
|
|
840
|
-
tolerance:
|
|
1383
|
+
tolerance: responseTolerance,
|
|
841
1384
|
property,
|
|
842
1385
|
attempts,
|
|
843
1386
|
adjustment_mode: adjustmentMode,
|
|
@@ -854,7 +1397,7 @@ export class ToolsInteract {
|
|
|
854
1397
|
target_state: {
|
|
855
1398
|
property,
|
|
856
1399
|
target_value: targetValue,
|
|
857
|
-
tolerance:
|
|
1400
|
+
tolerance: responseTolerance
|
|
858
1401
|
},
|
|
859
1402
|
actual_state: actualState,
|
|
860
1403
|
within_tolerance: false,
|
|
@@ -972,8 +1515,8 @@ export class ToolsInteract {
|
|
|
972
1515
|
return null
|
|
973
1516
|
}
|
|
974
1517
|
|
|
975
|
-
if (!selector && !element_id) {
|
|
976
|
-
return buildFailure('ELEMENT_NOT_FOUND', 'selector or element_id is required', null, undefined, null, 0, 'gesture', false)
|
|
1518
|
+
if ((!selector && !element_id) || (selector && element_id)) {
|
|
1519
|
+
return buildFailure('ELEMENT_NOT_FOUND', 'exactly one of selector or element_id is required', null, undefined, null, 0, 'gesture', false)
|
|
977
1520
|
}
|
|
978
1521
|
|
|
979
1522
|
if (selector && !element_id) {
|
|
@@ -1035,8 +1578,10 @@ export class ToolsInteract {
|
|
|
1035
1578
|
let currentDevice: any = undefined
|
|
1036
1579
|
let attemptCount = 0
|
|
1037
1580
|
let cachedResolvedMatch: { el: UiElement, idx: number } | null = null
|
|
1581
|
+
let coordinateTapsAwaitingFreshGesture = false
|
|
1038
1582
|
|
|
1039
|
-
|
|
1583
|
+
adjustmentAttempts:
|
|
1584
|
+
for (let attempt = 0; attempt < attemptsLimit && attemptCount < attemptsLimit; attempt++) {
|
|
1040
1585
|
const resolved: {
|
|
1041
1586
|
tree: any
|
|
1042
1587
|
device: any
|
|
@@ -1062,11 +1607,44 @@ export class ToolsInteract {
|
|
|
1062
1607
|
resolvedTarget = resolved.resolvedTarget
|
|
1063
1608
|
const currentEl: UiElement = resolved.match.el
|
|
1064
1609
|
cachedResolvedMatch = resolved.match
|
|
1065
|
-
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1610
|
+
|
|
1611
|
+
const adjustableActionability = ToolsInteract._isAdjustableActionable(currentEl, storedResolvedTarget?.stable_id)
|
|
1612
|
+
if (!adjustableActionability.actionable) {
|
|
1613
|
+
return buildFailure(
|
|
1614
|
+
adjustableActionability.failureCode ?? 'ELEMENT_NOT_INTERACTABLE',
|
|
1615
|
+
adjustableActionability.reason ?? 'adjustable control is not actionable',
|
|
1616
|
+
resolvedTarget,
|
|
1617
|
+
currentDevice,
|
|
1618
|
+
lastObservedState,
|
|
1619
|
+
attemptCount,
|
|
1620
|
+
lastAdjustmentMode,
|
|
1621
|
+
true
|
|
1622
|
+
)
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
const elementsForInference = Array.isArray(resolved.tree?.elements)
|
|
1626
|
+
? resolved.tree.elements as UiElement[]
|
|
1627
|
+
: cachedResolvedMatch
|
|
1628
|
+
? [cachedResolvedMatch.el]
|
|
1629
|
+
: [currentEl]
|
|
1630
|
+
const inferredSliderState = ToolsInteract._inferLabeledSliderState(elementsForInference, currentEl)
|
|
1631
|
+
const nativeBounds = ToolsInteract._normalizeBounds(currentEl.bounds)
|
|
1632
|
+
const bounds = inferredSliderState?.touch_bounds ?? nativeBounds
|
|
1633
|
+
const valueRange = ToolsInteract._readNumericControlRange(currentEl) ?? inferredSliderState?.value_range ?? null
|
|
1634
|
+
const step = ToolsInteract._readControlStep(currentEl)
|
|
1635
|
+
const normalizedTargetValue = valueRange
|
|
1636
|
+
? ToolsInteract._quantizeTargetValue(targetValue, valueRange.min, valueRange.max, step)
|
|
1637
|
+
: targetValue
|
|
1638
|
+
const effectiveTolerance = requestedTolerance ?? ToolsInteract._defaultAdjustTolerance(valueRange, step)
|
|
1639
|
+
const currentValue = ToolsInteract._readNumericControlValue(currentEl, property) ?? inferredSliderState?.current ?? null
|
|
1068
1640
|
const actualState = currentValue !== null
|
|
1069
|
-
? {
|
|
1641
|
+
? {
|
|
1642
|
+
property,
|
|
1643
|
+
value: currentValue,
|
|
1644
|
+
raw_value: typeof currentEl.state?.raw_value === 'number'
|
|
1645
|
+
? currentEl.state.raw_value
|
|
1646
|
+
: inferredSliderState?.current
|
|
1647
|
+
}
|
|
1070
1648
|
: null
|
|
1071
1649
|
|
|
1072
1650
|
lastObservedState = actualState
|
|
@@ -1083,10 +1661,23 @@ export class ToolsInteract {
|
|
|
1083
1661
|
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'adjust_control currently supports numeric value and raw_value properties only', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false)
|
|
1084
1662
|
}
|
|
1085
1663
|
|
|
1086
|
-
|
|
1664
|
+
const isAdjustableControl = ToolsInteract._isAdjustableControl(currentEl) || !!inferredSliderState
|
|
1665
|
+
if (!isAdjustableControl) {
|
|
1666
|
+
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'target is not an adjustable control', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false)
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
if (currentValue === null) {
|
|
1670
|
+
return buildFailure('UNKNOWN', 'current numeric value is unavailable', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false)
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
if (valueRange && (targetValue < valueRange.min || targetValue > valueRange.max)) {
|
|
1674
|
+
return buildFailure('CONTROL_CONVERGENCE_FAILED', `targetValue ${targetValue} is outside the control range ${valueRange.min}..${valueRange.max}`, resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false)
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
if (currentValue !== null && Math.abs(currentValue - normalizedTargetValue) <= effectiveTolerance) {
|
|
1087
1678
|
recordTraceStep('verify', 'success', {
|
|
1088
1679
|
property,
|
|
1089
|
-
target_value:
|
|
1680
|
+
target_value: normalizedTargetValue,
|
|
1090
1681
|
actual_state: actualState,
|
|
1091
1682
|
reason: 'control already within tolerance'
|
|
1092
1683
|
})
|
|
@@ -1101,8 +1692,8 @@ export class ToolsInteract {
|
|
|
1101
1692
|
uiFingerprintBefore: fingerprintBefore,
|
|
1102
1693
|
uiFingerprintAfter,
|
|
1103
1694
|
details: {
|
|
1104
|
-
target_value:
|
|
1105
|
-
tolerance:
|
|
1695
|
+
target_value: normalizedTargetValue,
|
|
1696
|
+
tolerance: effectiveTolerance,
|
|
1106
1697
|
property,
|
|
1107
1698
|
attempts: attemptCount,
|
|
1108
1699
|
adjustment_mode: 'semantic',
|
|
@@ -1118,8 +1709,8 @@ export class ToolsInteract {
|
|
|
1118
1709
|
...base,
|
|
1119
1710
|
target_state: {
|
|
1120
1711
|
property,
|
|
1121
|
-
target_value:
|
|
1122
|
-
tolerance:
|
|
1712
|
+
target_value: normalizedTargetValue,
|
|
1713
|
+
tolerance: effectiveTolerance
|
|
1123
1714
|
},
|
|
1124
1715
|
actual_state: actualState,
|
|
1125
1716
|
within_tolerance: true,
|
|
@@ -1129,50 +1720,48 @@ export class ToolsInteract {
|
|
|
1129
1720
|
}
|
|
1130
1721
|
}
|
|
1131
1722
|
|
|
1132
|
-
if (!ToolsInteract._isAdjustableControl(currentEl)) {
|
|
1133
|
-
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'target is not an adjustable control', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false)
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
1723
|
if (!bounds) {
|
|
1137
1724
|
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'adjustable control has no bounds', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false)
|
|
1138
1725
|
}
|
|
1139
1726
|
|
|
1140
|
-
const min = typeof valueRange?.min === 'number' ? valueRange.min : null
|
|
1141
|
-
const max = typeof valueRange?.max === 'number' ? valueRange.max : null
|
|
1142
|
-
if (min === null || max === null || !Number.isFinite(min) || !Number.isFinite(max) || max <= min) {
|
|
1143
|
-
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'value_range unavailable', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false)
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
if (targetValue < min || targetValue > max) {
|
|
1147
|
-
return buildFailure('UNKNOWN', `targetValue ${targetValue} is outside the control range ${min}..${max}`, resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false)
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
const axis = ToolsInteract._controlAxis(currentEl, bounds)
|
|
1151
|
-
const targetPoint = ToolsInteract._buildConservativeControlPoint(bounds, targetValue, currentValue, min, max, axis)
|
|
1152
|
-
const currentPoint = currentValue !== null
|
|
1153
|
-
? ToolsInteract._buildControlPoint(bounds, (currentValue - min) / (max - min), axis)
|
|
1154
|
-
: ToolsInteract._buildControlPoint(bounds, 0.5, axis)
|
|
1155
|
-
const probePoints = ToolsInteract._buildAdjustmentProbePoints(bounds, targetValue, currentValue, min, max, axis)
|
|
1156
|
-
|
|
1157
1727
|
const runVerification = async (): Promise<{
|
|
1158
1728
|
verification: any
|
|
1159
1729
|
observedState: { property: string; value: number | null; raw_value?: number | null } | null
|
|
1160
1730
|
withinTolerance: boolean
|
|
1161
1731
|
}> => {
|
|
1732
|
+
if (inferredSliderState) {
|
|
1733
|
+
const tree = await ToolsObserve.getUITreeHandler({ platform: resolvedPlatform, deviceId: resolvedDeviceId }) as any
|
|
1734
|
+
const elements = Array.isArray(tree?.elements) ? tree.elements as UiElement[] : []
|
|
1735
|
+
const refreshed = ToolsInteract._inferLabeledSliderState(elements, currentEl)
|
|
1736
|
+
const observedValue = refreshed?.current ?? null
|
|
1737
|
+
const observedState = observedValue !== null
|
|
1738
|
+
? { property, value: observedValue, raw_value: observedValue }
|
|
1739
|
+
: actualState
|
|
1740
|
+
|
|
1741
|
+
return {
|
|
1742
|
+
verification: {
|
|
1743
|
+
success: observedValue !== null && Math.abs(observedValue - normalizedTargetValue) <= effectiveTolerance,
|
|
1744
|
+
observed_state: observedState,
|
|
1745
|
+
reason: observedValue !== null
|
|
1746
|
+
? 'visible labeled value read from nearby slider labels'
|
|
1747
|
+
: 'visible labeled value unavailable'
|
|
1748
|
+
},
|
|
1749
|
+
observedState,
|
|
1750
|
+
withinTolerance: observedValue !== null && Math.abs(observedValue - normalizedTargetValue) <= effectiveTolerance
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1162
1754
|
const verification = await ToolsInteract.expectStateHandler({
|
|
1163
1755
|
element_id: resolvedTarget?.elementId ?? element_id,
|
|
1164
1756
|
selector: selector ?? undefined,
|
|
1165
1757
|
property,
|
|
1166
|
-
expected:
|
|
1758
|
+
expected: normalizedTargetValue,
|
|
1167
1759
|
platform: resolvedPlatform,
|
|
1168
1760
|
deviceId: resolvedDeviceId
|
|
1169
1761
|
}) as any
|
|
1170
1762
|
|
|
1171
|
-
const observedValue =
|
|
1172
|
-
|
|
1173
|
-
: typeof verification?.observed_state?.raw_value === 'number'
|
|
1174
|
-
? verification.observed_state.raw_value
|
|
1175
|
-
: null
|
|
1763
|
+
const observedValue = ToolsInteract._parseNumericControlValue(verification?.observed_state?.value) ??
|
|
1764
|
+
ToolsInteract._parseNumericControlValue(verification?.observed_state?.raw_value)
|
|
1176
1765
|
const observedState = observedValue !== null
|
|
1177
1766
|
? {
|
|
1178
1767
|
property,
|
|
@@ -1184,7 +1773,7 @@ export class ToolsInteract {
|
|
|
1184
1773
|
return {
|
|
1185
1774
|
verification,
|
|
1186
1775
|
observedState,
|
|
1187
|
-
withinTolerance: observedValue !== null && Math.abs(observedValue -
|
|
1776
|
+
withinTolerance: observedValue !== null && Math.abs(observedValue - normalizedTargetValue) <= effectiveTolerance
|
|
1188
1777
|
}
|
|
1189
1778
|
}
|
|
1190
1779
|
|
|
@@ -1193,86 +1782,276 @@ export class ToolsInteract {
|
|
|
1193
1782
|
let verification: any = null
|
|
1194
1783
|
let verificationResult: any = { verification: null, observedState: actualState, withinTolerance: false }
|
|
1195
1784
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1785
|
+
const directActionSupported = ToolsInteract._hasDirectAdjustmentAction(currentEl)
|
|
1786
|
+
if (directActionSupported && attemptCount < attemptsLimit) {
|
|
1787
|
+
const { interact, resolved: actionResolved } = await ToolsInteract.getInteractionService(resolvedPlatform, resolvedDeviceId)
|
|
1788
|
+
if (typeof interact.setAdjustableValue === 'function') {
|
|
1789
|
+
lastAdjustmentMode = 'semantic'
|
|
1790
|
+
recordTraceStep('execute', 'retry', {
|
|
1791
|
+
attempt: attemptCount + 1,
|
|
1792
|
+
mode: 'semantic',
|
|
1793
|
+
strategy: 'direct_set_value',
|
|
1794
|
+
target_value: normalizedTargetValue
|
|
1795
|
+
})
|
|
1796
|
+
const actionResult = await interact.setAdjustableValue({
|
|
1797
|
+
deviceId: actionResolved.id,
|
|
1798
|
+
target: resolvedTarget,
|
|
1799
|
+
property,
|
|
1800
|
+
value: normalizedTargetValue
|
|
1801
|
+
})
|
|
1802
|
+
attemptCount++
|
|
1803
|
+
actionDevice = actionResult?.device ?? actionDevice
|
|
1804
|
+
|
|
1805
|
+
if (actionResult?.success !== false) {
|
|
1806
|
+
verificationResult = await runVerification()
|
|
1807
|
+
observedState = verificationResult.observedState
|
|
1808
|
+
lastObservedState = observedState
|
|
1809
|
+
recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
|
|
1810
|
+
attempt: attemptCount,
|
|
1811
|
+
property,
|
|
1812
|
+
target_value: normalizedTargetValue,
|
|
1813
|
+
actual_state: observedState,
|
|
1814
|
+
reason: verificationResult.verification?.reason ?? 'direct adjustment did not converge yet'
|
|
1815
|
+
})
|
|
1816
|
+
|
|
1817
|
+
if (verificationResult.withinTolerance) {
|
|
1818
|
+
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId)
|
|
1819
|
+
const base = buildActionExecutionResult({
|
|
1820
|
+
actionType,
|
|
1821
|
+
sourceModule: 'interact',
|
|
1822
|
+
device: actionDevice ?? currentDevice,
|
|
1823
|
+
selector: targetSelector,
|
|
1824
|
+
resolved: resolvedTarget,
|
|
1825
|
+
success: true,
|
|
1826
|
+
uiFingerprintBefore: fingerprintBefore,
|
|
1827
|
+
uiFingerprintAfter,
|
|
1828
|
+
details: {
|
|
1829
|
+
target_value: normalizedTargetValue,
|
|
1830
|
+
tolerance: effectiveTolerance,
|
|
1831
|
+
property,
|
|
1832
|
+
attempts: attemptCount,
|
|
1833
|
+
adjustment_mode: lastAdjustmentMode,
|
|
1834
|
+
actual_state: observedState,
|
|
1835
|
+
converged: true,
|
|
1836
|
+
within_tolerance: true,
|
|
1837
|
+
reason: verificationResult.verification?.reason ?? 'control converged to target value'
|
|
1838
|
+
},
|
|
1839
|
+
traceSteps
|
|
1840
|
+
}) as AdjustControlResponse
|
|
1841
|
+
|
|
1842
|
+
return {
|
|
1843
|
+
...base,
|
|
1844
|
+
target_state: {
|
|
1845
|
+
property,
|
|
1846
|
+
target_value: normalizedTargetValue,
|
|
1847
|
+
tolerance: effectiveTolerance
|
|
1848
|
+
},
|
|
1849
|
+
actual_state: observedState,
|
|
1850
|
+
within_tolerance: true,
|
|
1851
|
+
converged: true,
|
|
1852
|
+
attempts: attemptCount,
|
|
1853
|
+
adjustment_mode: lastAdjustmentMode
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1212
1856
|
|
|
1213
|
-
|
|
1857
|
+
cachedResolvedMatch = null
|
|
1858
|
+
if (attemptCount < attemptsLimit) {
|
|
1859
|
+
continue
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
const directions = ToolsInteract._adjustmentDirections(currentEl)
|
|
1866
|
+
if ((directions.increment || directions.decrement) && attemptCount < attemptsLimit) {
|
|
1867
|
+
const { interact, resolved: actionResolved } = await ToolsInteract.getInteractionService(resolvedPlatform, resolvedDeviceId)
|
|
1868
|
+
const adjustByDirection = typeof interact.adjustAccessibleValue === 'function'
|
|
1869
|
+
? interact.adjustAccessibleValue.bind(interact)
|
|
1870
|
+
: null
|
|
1871
|
+
while (adjustByDirection && attemptCount < attemptsLimit) {
|
|
1872
|
+
const direction = normalizedTargetValue > (observedState?.value ?? currentValue) ? 'increment' : 'decrement'
|
|
1873
|
+
if ((direction === 'increment' && !directions.increment) || (direction === 'decrement' && !directions.decrement)) break
|
|
1874
|
+
lastAdjustmentMode = 'semantic'
|
|
1214
1875
|
recordTraceStep('execute', 'retry', {
|
|
1876
|
+
attempt: attemptCount + 1,
|
|
1877
|
+
mode: 'semantic',
|
|
1878
|
+
strategy: 'accessibility_increment_decrement',
|
|
1879
|
+
direction
|
|
1880
|
+
})
|
|
1881
|
+
const actionResult = await adjustByDirection({
|
|
1882
|
+
deviceId: actionResolved.id,
|
|
1883
|
+
target: resolvedTarget,
|
|
1884
|
+
property,
|
|
1885
|
+
direction
|
|
1886
|
+
})
|
|
1887
|
+
attemptCount++
|
|
1888
|
+
actionDevice = actionResult?.device ?? actionDevice
|
|
1889
|
+
if (actionResult?.success === false) break
|
|
1890
|
+
|
|
1891
|
+
verificationResult = await runVerification()
|
|
1892
|
+
observedState = verificationResult.observedState
|
|
1893
|
+
lastObservedState = observedState
|
|
1894
|
+
recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
|
|
1215
1895
|
attempt: attemptCount,
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1896
|
+
property,
|
|
1897
|
+
target_value: normalizedTargetValue,
|
|
1898
|
+
actual_state: observedState,
|
|
1899
|
+
reason: verificationResult.verification?.reason ?? 'incremental adjustment did not converge yet'
|
|
1219
1900
|
})
|
|
1220
|
-
|
|
1901
|
+
|
|
1902
|
+
if (verificationResult.withinTolerance) {
|
|
1903
|
+
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId)
|
|
1904
|
+
const base = buildActionExecutionResult({
|
|
1905
|
+
actionType,
|
|
1906
|
+
sourceModule: 'interact',
|
|
1907
|
+
device: actionDevice ?? currentDevice,
|
|
1908
|
+
selector: targetSelector,
|
|
1909
|
+
resolved: resolvedTarget,
|
|
1910
|
+
success: true,
|
|
1911
|
+
uiFingerprintBefore: fingerprintBefore,
|
|
1912
|
+
uiFingerprintAfter,
|
|
1913
|
+
details: {
|
|
1914
|
+
target_value: normalizedTargetValue,
|
|
1915
|
+
tolerance: effectiveTolerance,
|
|
1916
|
+
property,
|
|
1917
|
+
attempts: attemptCount,
|
|
1918
|
+
adjustment_mode: lastAdjustmentMode,
|
|
1919
|
+
actual_state: observedState,
|
|
1920
|
+
converged: true,
|
|
1921
|
+
within_tolerance: true,
|
|
1922
|
+
reason: verificationResult.verification?.reason ?? 'control converged to target value'
|
|
1923
|
+
},
|
|
1924
|
+
traceSteps
|
|
1925
|
+
}) as AdjustControlResponse
|
|
1926
|
+
|
|
1927
|
+
return {
|
|
1928
|
+
...base,
|
|
1929
|
+
target_state: {
|
|
1930
|
+
property,
|
|
1931
|
+
target_value: normalizedTargetValue,
|
|
1932
|
+
tolerance: effectiveTolerance
|
|
1933
|
+
},
|
|
1934
|
+
actual_state: observedState,
|
|
1935
|
+
within_tolerance: true,
|
|
1936
|
+
converged: true,
|
|
1937
|
+
attempts: attemptCount,
|
|
1938
|
+
adjustment_mode: lastAdjustmentMode
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
cachedResolvedMatch = null
|
|
1943
|
+
if (attemptCount < attemptsLimit) {
|
|
1944
|
+
continue adjustmentAttempts
|
|
1945
|
+
}
|
|
1221
1946
|
}
|
|
1947
|
+
}
|
|
1222
1948
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
|
|
1227
|
-
attempt: attemptCount,
|
|
1228
|
-
property,
|
|
1229
|
-
target_value: targetValue,
|
|
1230
|
-
actual_state: observedState,
|
|
1231
|
-
reason: verificationResult.verification?.reason ?? 'control did not converge yet'
|
|
1232
|
-
})
|
|
1949
|
+
if (!bounds) {
|
|
1950
|
+
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'adjustable control has no bounds', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false)
|
|
1951
|
+
}
|
|
1233
1952
|
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1953
|
+
if (!valueRange) {
|
|
1954
|
+
return buildFailure('UNKNOWN', 'value_range unavailable', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false)
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
const min = valueRange.min
|
|
1958
|
+
const max = valueRange.max
|
|
1959
|
+
const axis = ToolsInteract._controlAxis(currentEl, bounds)
|
|
1960
|
+
const targetPoint = ToolsInteract._buildConservativeControlPoint(bounds, normalizedTargetValue, currentValue, min, max, axis)
|
|
1961
|
+
const currentPoint = ToolsInteract._buildControlPoint(bounds, (currentValue - min) / (max - min), axis)
|
|
1962
|
+
if (!coordinateTapsAwaitingFreshGesture) {
|
|
1963
|
+
const probePoints = inferredSliderState
|
|
1964
|
+
? [targetPoint]
|
|
1965
|
+
: ToolsInteract._buildAdjustmentProbePoints(bounds, normalizedTargetValue, currentValue, min, max, axis)
|
|
1966
|
+
|
|
1967
|
+
for (let i = 0; i < probePoints.length && attemptCount < attemptsLimit; i++) {
|
|
1968
|
+
const probePoint = probePoints[i]
|
|
1969
|
+
lastAdjustmentMode = 'coordinate'
|
|
1970
|
+
recordTraceStep('execute', 'retry', {
|
|
1971
|
+
attempt: attemptCount + 1,
|
|
1972
|
+
mode: 'coordinate',
|
|
1973
|
+
point: probePoint
|
|
1974
|
+
})
|
|
1975
|
+
const actionResult = await ToolsInteract.tapHandler({
|
|
1976
|
+
platform: resolvedPlatform,
|
|
1977
|
+
x: probePoint.x,
|
|
1978
|
+
y: probePoint.y,
|
|
1979
|
+
deviceId: resolvedDeviceId
|
|
1980
|
+
})
|
|
1981
|
+
attemptCount++
|
|
1982
|
+
actionDevice = actionResult.device ?? actionDevice
|
|
1983
|
+
|
|
1984
|
+
if (!actionResult.success) {
|
|
1985
|
+
recordTraceStep('execute', 'retry', {
|
|
1986
|
+
attempt: attemptCount,
|
|
1987
|
+
mode: 'coordinate',
|
|
1988
|
+
point: probePoint,
|
|
1989
|
+
success: false
|
|
1990
|
+
})
|
|
1991
|
+
continue
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
verificationResult = await runVerification()
|
|
1995
|
+
observedState = verificationResult.observedState
|
|
1996
|
+
lastObservedState = observedState
|
|
1997
|
+
recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
|
|
1998
|
+
attempt: attemptCount,
|
|
1999
|
+
property,
|
|
2000
|
+
target_value: normalizedTargetValue,
|
|
2001
|
+
actual_state: observedState,
|
|
2002
|
+
reason: verificationResult.verification?.reason ?? 'control did not converge yet'
|
|
2003
|
+
})
|
|
2004
|
+
|
|
2005
|
+
if (verificationResult.withinTolerance) {
|
|
2006
|
+
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId)
|
|
2007
|
+
const base = buildActionExecutionResult({
|
|
2008
|
+
actionType,
|
|
2009
|
+
sourceModule: 'interact',
|
|
2010
|
+
device: actionDevice ?? currentDevice,
|
|
2011
|
+
selector: targetSelector,
|
|
2012
|
+
resolved: resolvedTarget,
|
|
2013
|
+
success: true,
|
|
2014
|
+
uiFingerprintBefore: fingerprintBefore,
|
|
2015
|
+
uiFingerprintAfter,
|
|
2016
|
+
details: {
|
|
2017
|
+
target_value: normalizedTargetValue,
|
|
2018
|
+
tolerance: effectiveTolerance,
|
|
2019
|
+
property,
|
|
2020
|
+
attempts: attemptCount,
|
|
2021
|
+
adjustment_mode: lastAdjustmentMode,
|
|
2022
|
+
actual_state: observedState,
|
|
2023
|
+
converged: true,
|
|
2024
|
+
within_tolerance: true,
|
|
2025
|
+
reason: verificationResult.verification?.reason ?? 'control converged to target value'
|
|
2026
|
+
},
|
|
2027
|
+
traceSteps
|
|
2028
|
+
}) as AdjustControlResponse
|
|
2029
|
+
|
|
2030
|
+
return {
|
|
2031
|
+
...base,
|
|
2032
|
+
target_state: {
|
|
2033
|
+
property,
|
|
2034
|
+
target_value: normalizedTargetValue,
|
|
2035
|
+
tolerance: effectiveTolerance
|
|
2036
|
+
},
|
|
1251
2037
|
actual_state: observedState,
|
|
1252
|
-
converged: true,
|
|
1253
2038
|
within_tolerance: true,
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
2039
|
+
converged: true,
|
|
2040
|
+
attempts: attemptCount,
|
|
2041
|
+
adjustment_mode: lastAdjustmentMode
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
1258
2044
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
target_value: targetValue,
|
|
1264
|
-
tolerance: normalizedTolerance
|
|
1265
|
-
},
|
|
1266
|
-
actual_state: observedState,
|
|
1267
|
-
within_tolerance: true,
|
|
1268
|
-
converged: true,
|
|
1269
|
-
attempts: attemptCount,
|
|
1270
|
-
adjustment_mode: lastAdjustmentMode
|
|
2045
|
+
coordinateTapsAwaitingFreshGesture = true
|
|
2046
|
+
cachedResolvedMatch = null
|
|
2047
|
+
if (attemptCount < attemptsLimit) {
|
|
2048
|
+
continue adjustmentAttempts
|
|
1271
2049
|
}
|
|
1272
2050
|
}
|
|
1273
2051
|
}
|
|
1274
2052
|
|
|
1275
|
-
if (
|
|
2053
|
+
if (attemptCount < attemptsLimit) {
|
|
2054
|
+
coordinateTapsAwaitingFreshGesture = false
|
|
1276
2055
|
lastAdjustmentMode = 'gesture'
|
|
1277
2056
|
recordTraceStep('execute', 'retry', {
|
|
1278
2057
|
attempt: attemptCount + 1,
|
|
@@ -1286,7 +2065,7 @@ export class ToolsInteract {
|
|
|
1286
2065
|
y1: currentPoint.y,
|
|
1287
2066
|
x2: targetPoint.x,
|
|
1288
2067
|
y2: targetPoint.y,
|
|
1289
|
-
duration: 220,
|
|
2068
|
+
duration: inferredSliderState ? 520 : 220,
|
|
1290
2069
|
deviceId: resolvedDeviceId
|
|
1291
2070
|
})
|
|
1292
2071
|
attemptCount++
|
|
@@ -1308,7 +2087,7 @@ export class ToolsInteract {
|
|
|
1308
2087
|
recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
|
|
1309
2088
|
attempt: attemptCount,
|
|
1310
2089
|
property,
|
|
1311
|
-
target_value:
|
|
2090
|
+
target_value: normalizedTargetValue,
|
|
1312
2091
|
actual_state: observedState,
|
|
1313
2092
|
reason: verificationResult.verification?.reason ?? 'gesture adjustment did not converge yet'
|
|
1314
2093
|
})
|
|
@@ -1325,8 +2104,8 @@ export class ToolsInteract {
|
|
|
1325
2104
|
uiFingerprintBefore: fingerprintBefore,
|
|
1326
2105
|
uiFingerprintAfter,
|
|
1327
2106
|
details: {
|
|
1328
|
-
target_value:
|
|
1329
|
-
tolerance:
|
|
2107
|
+
target_value: normalizedTargetValue,
|
|
2108
|
+
tolerance: effectiveTolerance,
|
|
1330
2109
|
property,
|
|
1331
2110
|
attempts: attemptCount,
|
|
1332
2111
|
adjustment_mode: lastAdjustmentMode,
|
|
@@ -1342,8 +2121,8 @@ export class ToolsInteract {
|
|
|
1342
2121
|
...base,
|
|
1343
2122
|
target_state: {
|
|
1344
2123
|
property,
|
|
1345
|
-
target_value:
|
|
1346
|
-
tolerance:
|
|
2124
|
+
target_value: normalizedTargetValue,
|
|
2125
|
+
tolerance: effectiveTolerance
|
|
1347
2126
|
},
|
|
1348
2127
|
actual_state: observedState,
|
|
1349
2128
|
within_tolerance: true,
|
|
@@ -1361,7 +2140,7 @@ export class ToolsInteract {
|
|
|
1361
2140
|
recordTraceStep('verify', 'success', {
|
|
1362
2141
|
attempt: attemptCount,
|
|
1363
2142
|
property,
|
|
1364
|
-
target_value:
|
|
2143
|
+
target_value: normalizedTargetValue,
|
|
1365
2144
|
actual_state: observedState,
|
|
1366
2145
|
reason: verification?.reason ?? 'control converged to target value'
|
|
1367
2146
|
})
|
|
@@ -1376,8 +2155,8 @@ export class ToolsInteract {
|
|
|
1376
2155
|
uiFingerprintBefore: fingerprintBefore,
|
|
1377
2156
|
uiFingerprintAfter,
|
|
1378
2157
|
details: {
|
|
1379
|
-
target_value:
|
|
1380
|
-
tolerance:
|
|
2158
|
+
target_value: normalizedTargetValue,
|
|
2159
|
+
tolerance: effectiveTolerance,
|
|
1381
2160
|
property,
|
|
1382
2161
|
attempts: attemptCount,
|
|
1383
2162
|
adjustment_mode: lastAdjustmentMode,
|
|
@@ -1393,8 +2172,8 @@ export class ToolsInteract {
|
|
|
1393
2172
|
...base,
|
|
1394
2173
|
target_state: {
|
|
1395
2174
|
property,
|
|
1396
|
-
target_value:
|
|
1397
|
-
tolerance:
|
|
2175
|
+
target_value: normalizedTargetValue,
|
|
2176
|
+
tolerance: effectiveTolerance
|
|
1398
2177
|
},
|
|
1399
2178
|
actual_state: observedState,
|
|
1400
2179
|
within_tolerance: true,
|
|
@@ -1404,23 +2183,25 @@ export class ToolsInteract {
|
|
|
1404
2183
|
}
|
|
1405
2184
|
}
|
|
1406
2185
|
|
|
1407
|
-
cachedResolvedMatch =
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
...
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
2186
|
+
cachedResolvedMatch = inferredSliderState
|
|
2187
|
+
? null
|
|
2188
|
+
: {
|
|
2189
|
+
el: {
|
|
2190
|
+
...currentEl,
|
|
2191
|
+
state: {
|
|
2192
|
+
...(currentEl.state ?? null),
|
|
2193
|
+
...(observedState ? {
|
|
2194
|
+
[observedState.property]: observedState.value,
|
|
2195
|
+
raw_value: observedState.raw_value ?? observedState.value
|
|
2196
|
+
} : {})
|
|
2197
|
+
}
|
|
2198
|
+
},
|
|
2199
|
+
idx: resolved.match.idx
|
|
2200
|
+
}
|
|
1420
2201
|
}
|
|
1421
2202
|
|
|
1422
2203
|
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId)
|
|
1423
|
-
return buildFailure('
|
|
2204
|
+
return buildFailure('CONTROL_CONVERGENCE_FAILED', 'control did not converge within the allotted attempts', resolvedTarget, currentDevice, lastObservedState, attemptCount, lastAdjustmentMode, true, uiFingerprintAfter)
|
|
1424
2205
|
}
|
|
1425
2206
|
|
|
1426
2207
|
static async swipeHandler({ platform = 'android', x1, y1, x2, y2, duration, deviceId }: { platform?: 'android' | 'ios', x1: number, y1: number, x2: number, y2: number, duration: number, deviceId?: string }) {
|
|
@@ -2021,33 +2802,79 @@ export class ToolsInteract {
|
|
|
2021
2802
|
deviceId,
|
|
2022
2803
|
timeout_ms = 60000,
|
|
2023
2804
|
stability_window_ms = 300,
|
|
2024
|
-
expected_change
|
|
2805
|
+
expected_change,
|
|
2806
|
+
scope = 'screen',
|
|
2807
|
+
target = null
|
|
2025
2808
|
}: {
|
|
2026
2809
|
platform?: 'android' | 'ios',
|
|
2027
2810
|
deviceId?: string,
|
|
2028
2811
|
timeout_ms?: number,
|
|
2029
2812
|
stability_window_ms?: number,
|
|
2030
|
-
expected_change?: 'hierarchy_diff' | 'text_change' | 'state_change'
|
|
2813
|
+
expected_change?: 'hierarchy_diff' | 'text_change' | 'state_change',
|
|
2814
|
+
scope?: 'screen' | 'subtree',
|
|
2815
|
+
target?: string | null
|
|
2031
2816
|
}): Promise<WaitForUIChangeResponse> {
|
|
2032
2817
|
const start = Date.now()
|
|
2033
2818
|
const pollIntervalMs = 300
|
|
2034
2819
|
const stabilityWindow = Math.max(0, typeof stability_window_ms === 'number' ? stability_window_ms : 300)
|
|
2035
2820
|
let baseline: UiChangeSignatureSet | null = null
|
|
2821
|
+
let baselineScope: UiChangeScopeResult | null = null
|
|
2036
2822
|
let lastObservedRevision: number | null = null
|
|
2037
2823
|
let lastLoadingState: any = null
|
|
2824
|
+
let lastSnapshotFreshnessMs: number | null = null
|
|
2038
2825
|
let candidateSignatures: UiChangeSignatureSet | null = null
|
|
2039
2826
|
let candidateObservedChange: 'hierarchy_diff' | 'text_change' | 'state_change' | null = null
|
|
2040
2827
|
let candidateSinceMs: number | null = null
|
|
2828
|
+
let lastChangeSummary: ReturnType<typeof ToolsInteract._summarizeUiChangeDelta> | null = null
|
|
2829
|
+
let lastScopeResolution: UiChangeScopeResolution = {
|
|
2830
|
+
scope: scope === 'subtree' ? 'subtree' : 'screen',
|
|
2831
|
+
target: target && typeof target === 'string' ? target : null,
|
|
2832
|
+
resolved: scope !== 'subtree',
|
|
2833
|
+
resolvedIndex: null,
|
|
2834
|
+
resolvedStableId: null,
|
|
2835
|
+
reason: scope === 'subtree' ? 'target not resolved yet' : 'screen scope'
|
|
2836
|
+
}
|
|
2041
2837
|
|
|
2042
2838
|
while (Date.now() - start < timeout_ms) {
|
|
2043
2839
|
try {
|
|
2044
2840
|
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId }) as any
|
|
2045
|
-
const
|
|
2841
|
+
const scopedTree = ToolsInteract._resolveUiChangeScope(tree, scope, target)
|
|
2842
|
+
if (scopedTree.error) {
|
|
2843
|
+
lastScopeResolution = scopedTree.resolution
|
|
2844
|
+
return {
|
|
2845
|
+
success: false,
|
|
2846
|
+
observed_change: null,
|
|
2847
|
+
snapshot_revision: typeof tree?.snapshot_revision === 'number' ? tree.snapshot_revision : lastObservedRevision ?? undefined,
|
|
2848
|
+
snapshot_freshness_ms: typeof tree?.captured_at_ms === 'number' ? Math.max(0, Date.now() - tree.captured_at_ms) : lastSnapshotFreshnessMs ?? null,
|
|
2849
|
+
timeout: true,
|
|
2850
|
+
elapsed_ms: Date.now() - start,
|
|
2851
|
+
expected_change,
|
|
2852
|
+
loading_state: tree?.loading_state ?? lastLoadingState ?? null,
|
|
2853
|
+
scope: scopedTree.resolution.scope,
|
|
2854
|
+
target: scopedTree.resolution.target,
|
|
2855
|
+
stability_state: 'transient',
|
|
2856
|
+
change_summary: lastChangeSummary,
|
|
2857
|
+
reason: scopedTree.error.message,
|
|
2858
|
+
error: scopedTree.error
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
const scopedElements = scopedTree.elements
|
|
2863
|
+
const scopedSignatureTree = {
|
|
2864
|
+
...tree,
|
|
2865
|
+
elements: scopedElements
|
|
2866
|
+
}
|
|
2867
|
+
const signatures = ToolsInteract._buildUiChangeSignatures(scopedSignatureTree)
|
|
2046
2868
|
lastObservedRevision = typeof tree?.snapshot_revision === 'number' ? tree.snapshot_revision : lastObservedRevision
|
|
2047
2869
|
lastLoadingState = tree?.loading_state ?? lastLoadingState
|
|
2870
|
+
lastSnapshotFreshnessMs = typeof tree?.captured_at_ms === 'number' ? Math.max(0, Date.now() - tree.captured_at_ms) : lastSnapshotFreshnessMs
|
|
2871
|
+
lastChangeSummary = baseline ? ToolsInteract._summarizeUiChangeDelta((baselineScope?.elements ?? []), scopedElements) : lastChangeSummary
|
|
2872
|
+
lastScopeResolution = scopedTree.resolution
|
|
2873
|
+
baselineScope = baselineScope ?? scopedTree
|
|
2048
2874
|
|
|
2049
2875
|
if (!baseline) {
|
|
2050
2876
|
baseline = signatures
|
|
2877
|
+
baselineScope = scopedTree
|
|
2051
2878
|
} else {
|
|
2052
2879
|
const observedChange = ToolsInteract._matchesUiChange(expected_change, baseline, signatures)
|
|
2053
2880
|
if (observedChange) {
|
|
@@ -2059,20 +2886,25 @@ export class ToolsInteract {
|
|
|
2059
2886
|
|
|
2060
2887
|
const stableForMs = candidateSinceMs === null ? 0 : Date.now() - candidateSinceMs
|
|
2061
2888
|
if (stabilityWindow === 0 || stableForMs >= stabilityWindow) {
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2889
|
+
return {
|
|
2890
|
+
success: true,
|
|
2891
|
+
observed_change: candidateObservedChange ?? observedChange,
|
|
2892
|
+
snapshot_revision: lastObservedRevision ?? undefined,
|
|
2893
|
+
snapshot_freshness_ms: lastSnapshotFreshnessMs ?? null,
|
|
2894
|
+
timeout: false,
|
|
2895
|
+
elapsed_ms: Date.now() - start,
|
|
2896
|
+
expected_change,
|
|
2897
|
+
loading_state: lastLoadingState ?? null,
|
|
2898
|
+
scope: lastScopeResolution.scope,
|
|
2899
|
+
target: lastScopeResolution.target,
|
|
2900
|
+
stability_state: 'stable',
|
|
2901
|
+
change_summary: lastChangeSummary,
|
|
2902
|
+
reason: 'UI change observed'
|
|
2903
|
+
}
|
|
2071
2904
|
}
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
candidateObservedChange = null
|
|
2905
|
+
} else {
|
|
2906
|
+
candidateSignatures = null
|
|
2907
|
+
candidateObservedChange = null
|
|
2076
2908
|
candidateSinceMs = null
|
|
2077
2909
|
}
|
|
2078
2910
|
}
|
|
@@ -2087,10 +2919,15 @@ export class ToolsInteract {
|
|
|
2087
2919
|
success: false,
|
|
2088
2920
|
observed_change: null,
|
|
2089
2921
|
snapshot_revision: lastObservedRevision ?? undefined,
|
|
2922
|
+
snapshot_freshness_ms: lastSnapshotFreshnessMs ?? null,
|
|
2090
2923
|
timeout: true,
|
|
2091
2924
|
elapsed_ms: Date.now() - start,
|
|
2092
2925
|
expected_change,
|
|
2093
2926
|
loading_state: lastLoadingState ?? null,
|
|
2927
|
+
scope: lastScopeResolution.scope,
|
|
2928
|
+
target: lastScopeResolution.target,
|
|
2929
|
+
stability_state: 'transient',
|
|
2930
|
+
change_summary: lastChangeSummary,
|
|
2094
2931
|
reason: 'timeout'
|
|
2095
2932
|
}
|
|
2096
2933
|
}
|