@appilots/sdk 0.11.0 → 0.11.3
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/.build-meta.json +3 -3
- package/dist/{chunk-NA5H4IHH.mjs → chunk-2ZDELL54.mjs} +352 -114
- package/dist/{chunk-IZEWWAA7.js → chunk-5UVMNKPT.js} +8 -8
- package/dist/{chunk-3K7ORC4X.mjs → chunk-JKLE5SLN.mjs} +1 -1
- package/dist/{chunk-G6KI7KVH.mjs → chunk-ROXXCDIG.mjs} +299 -15
- package/dist/{chunk-55Y2AQE7.js → chunk-SUMIABJK.js} +299 -14
- package/dist/{chunk-2YPEQTL2.js → chunk-UFMTRGML.js} +622 -384
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.js +11 -11
- package/dist/hooks/index.mjs +2 -2
- package/dist/{index-BPZ0j71S.d.mts → index-BR-33WMN.d.mts} +112 -1
- package/dist/{index-BPZ0j71S.d.ts → index-BR-33WMN.d.ts} +112 -1
- package/dist/index.d.mts +28 -3
- package/dist/index.d.ts +28 -3
- package/dist/index.js +112 -82
- package/dist/index.mjs +45 -15
- package/dist/navigation/index.d.mts +42 -6
- package/dist/navigation/index.d.ts +42 -6
- package/dist/navigation/index.js +13 -13
- package/dist/navigation/index.mjs +2 -2
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getFiberRoot, appilotsDebugWarn, recordIntrospectionFailure, getCurrentScreen, walkFiber, getActiveRouteNames, clampSnapshotToWireLimits, deriveInteractionElements, rectForNode, toWireElement, attachElementIdsToChoiceGroups, elementRegistry, appilotsDebugLog, getLastWalkDiagnostics, SDK_VERSION, geometryStats, geometryAvailability, getNameManglingProbe, emptyDetectorCounts, getAutoTrackingState, getIntrospectionDiagnostics, getDefaultRegistry, useAppilotsContext, ChatSessionMachine, getCurrentScreenSignature, ActionQueueMachine, getScreenMetadata, setCurrentScreen, useResolvedRegistry, totalDetectorHits, listRegistry, getOpenNativeDialog, componentRegistry, getLastAnsweredNativeDialog, probeLoadingState, isListItemPressTarget, waitForLoadingSettle, describeValue, waitForScreenSettle, screenDepartedBaseline, routesBelongToSameFeature, actionPressTargetId, getNavigationStateSnapshot, getNavigationRef, isOptionalStepAutomationFailure, OPTIONAL_STEP_AUTOMATION_HINT, normalize3, matchScore, FORM_VALIDATION_TEXT_RE,
|
|
1
|
+
import { createSnapshotDeltaTracker, getFiberRoot, appilotsDebugWarn, recordIntrospectionFailure, getCurrentScreen, walkFiber, getActiveRouteNames, clampSnapshotToWireLimits, deriveInteractionElements, rectForNode, toWireElement, attachElementIdsToChoiceGroups, elementRegistry, appilotsDebugLog, getLastWalkDiagnostics, SDK_VERSION, geometryStats, geometryAvailability, getNameManglingProbe, emptyDetectorCounts, getAutoTrackingState, getIntrospectionDiagnostics, getDefaultRegistry, useAppilotsContext, ChatSessionMachine, getCurrentScreenSignature, ActionQueueMachine, getScreenMetadata, setCurrentScreen, useResolvedRegistry, totalDetectorHits, listRegistry, getOpenNativeDialog, componentRegistry, getLastAnsweredNativeDialog, probeLoadingState, isListItemPressTarget, waitForLoadingSettle, describeValue, waitForScreenSettle, screenDepartedBaseline, routesBelongToSameFeature, actionPressTargetId, getNavigationStateSnapshot, getNavigationRef, isOptionalStepAutomationFailure, OPTIONAL_STEP_AUTOMATION_HINT, isSwitchLike, idLooselyMatches, normalize3, matchScore, FORM_VALIDATION_TEXT_RE, parseStableElementId, shouldSkipSubtree, isAlwaysListContainer, isHeuristicListContainer, findListItems, getTypeName, foldForCompare, isModalFiber, switchValue, getAllScreens, snapshotShowsOpenCreateForm, isReferenceable, looksDestructiveActionLabel, isGenericSelectValue, FORM_WRITE_SUCCESS_TEXT_RE, TOUCHABLE_NAMES } from './chunk-ROXXCDIG.mjs';
|
|
2
2
|
import React, { useMemo, useRef, useSyncExternalStore, useEffect, useCallback, useState } from 'react';
|
|
3
3
|
import { Alert } from 'react-native';
|
|
4
4
|
|
|
@@ -270,6 +270,39 @@ function appilotsSelfCheck() {
|
|
|
270
270
|
};
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
// src/introspection/switchFlip.ts
|
|
274
|
+
function declaredState(props) {
|
|
275
|
+
if (typeof props?.value === "boolean") return props.value;
|
|
276
|
+
const aria = props?.["aria-checked"];
|
|
277
|
+
if (typeof aria === "boolean") return aria;
|
|
278
|
+
const checked = props?.accessibilityState?.checked;
|
|
279
|
+
return typeof checked === "boolean" ? checked : void 0;
|
|
280
|
+
}
|
|
281
|
+
function isReactNativeSwitch(fiber) {
|
|
282
|
+
return getTypeName(fiber?.type) === "Switch" || getTypeName(fiber?.elementType) === "Switch";
|
|
283
|
+
}
|
|
284
|
+
var SWITCH_FLIP_HANDLERS = ["onValueChange", "onChange", "onPress"];
|
|
285
|
+
function flipSwitchFiber(fiber, next) {
|
|
286
|
+
const props = fiber?.memoizedProps ?? {};
|
|
287
|
+
const onValueChange = props.onValueChange;
|
|
288
|
+
const onChange = props.onChange;
|
|
289
|
+
const onPress = props.onPress;
|
|
290
|
+
const handler = typeof onValueChange === "function" ? { fn: onValueChange, via: "onValueChange" } : typeof onChange === "function" ? { fn: onChange, via: "onChange" } : typeof onPress === "function" ? { fn: onPress, via: "onPress" } : null;
|
|
291
|
+
if (!handler) return { ok: false, reason: "no-handler" };
|
|
292
|
+
try {
|
|
293
|
+
if (handler.via === "onPress") {
|
|
294
|
+
if (declaredState(props) === next) return { ok: true, via: "onPress", noop: true };
|
|
295
|
+
handler.fn();
|
|
296
|
+
} else if (handler.via === "onChange") {
|
|
297
|
+
if (isReactNativeSwitch(fiber)) handler.fn({ nativeEvent: { value: next }, value: next });
|
|
298
|
+
else handler.fn(next);
|
|
299
|
+
} else handler.fn(next);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
return { ok: false, reason: "threw", error: err?.message ?? "switch handler threw" };
|
|
302
|
+
}
|
|
303
|
+
return { ok: true, via: handler.via };
|
|
304
|
+
}
|
|
305
|
+
|
|
273
306
|
// src/introspection/findInteractive.ts
|
|
274
307
|
function getTypeName2(type) {
|
|
275
308
|
if (!type) return null;
|
|
@@ -355,7 +388,7 @@ function shouldSkipSubtree2(fiber) {
|
|
|
355
388
|
return false;
|
|
356
389
|
}
|
|
357
390
|
var normalize = foldForCompare;
|
|
358
|
-
function
|
|
391
|
+
function scoreMatchDetail(fiber, queryNorm, queryRaw) {
|
|
359
392
|
const props = fiber.memoizedProps ?? {};
|
|
360
393
|
const candidates = [];
|
|
361
394
|
if (typeof props.testID === "string") candidates.push({ val: props.testID, weight: 100 });
|
|
@@ -378,13 +411,16 @@ function scoreMatch(fiber, queryNorm, queryRaw) {
|
|
|
378
411
|
else if (queryNorm && (vNorm.includes(queryNorm) || queryNorm.includes(vNorm)))
|
|
379
412
|
best = Math.max(best, weight - 20);
|
|
380
413
|
}
|
|
381
|
-
|
|
414
|
+
const named = best >= IDENTITY_MATCH_SCORE;
|
|
415
|
+
const disabledProp = props.disabled === true;
|
|
416
|
+
const disabledA11y = typeof props.accessibilityState === "object" && props.accessibilityState !== null && props.accessibilityState.disabled === true;
|
|
417
|
+
if (best > 0 && disabledProp) {
|
|
382
418
|
best = Math.max(1, best - 60);
|
|
383
419
|
}
|
|
384
|
-
if (best > 0 &&
|
|
420
|
+
if (best > 0 && disabledA11y) {
|
|
385
421
|
best = Math.max(1, best - 60);
|
|
386
422
|
}
|
|
387
|
-
return best;
|
|
423
|
+
return { score: best, named, disabled: disabledProp || disabledA11y };
|
|
388
424
|
}
|
|
389
425
|
function wrapTextInput(fiber) {
|
|
390
426
|
const getProps = () => fiber.memoizedProps ?? {};
|
|
@@ -421,13 +457,31 @@ function wrapSwitch(fiber) {
|
|
|
421
457
|
return {
|
|
422
458
|
kind: "toggle",
|
|
423
459
|
label: getProps().accessibilityLabel ?? getProps().testID,
|
|
424
|
-
|
|
460
|
+
// `switchValue` reads `accessibilityState.checked` when there is no
|
|
461
|
+
// `value` prop. A CUSTOM switch (a Pressable with
|
|
462
|
+
// `accessibilityRole="switch"`) has only the former, and reporting
|
|
463
|
+
// `false` for a control that is on is worse than reporting nothing:
|
|
464
|
+
// the caller inverts this to decide where to move.
|
|
465
|
+
getValue: () => switchValue(getProps()),
|
|
425
466
|
setValue: (value) => {
|
|
426
|
-
const { onValueChange, onChange } = getProps();
|
|
427
467
|
const boolVal = typeof value === "boolean" ? value : value === "true" || value === true;
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
468
|
+
const flipped = flipSwitchFiber(fiber, boolVal);
|
|
469
|
+
if (flipped.ok) return;
|
|
470
|
+
if (flipped.reason === "no-handler") {
|
|
471
|
+
throw new Error(`Switch exposes none of: ${SWITCH_FLIP_HANDLERS.join(", ")}`);
|
|
472
|
+
}
|
|
473
|
+
throw new Error(flipped.error);
|
|
474
|
+
},
|
|
475
|
+
// A custom switch is a `Pressable` underneath, so it may well declare
|
|
476
|
+
// its own long press (hold to configure, hold for a shortcut menu).
|
|
477
|
+
// Classifying it as a toggle took it out of the `kind: 'target'`
|
|
478
|
+
// lookup, and without this the long press would fall through to the
|
|
479
|
+
// best remaining candidate — the card that WRAPS it.
|
|
480
|
+
longPress: () => {
|
|
481
|
+
const { onLongPress } = getProps();
|
|
482
|
+
if (typeof onLongPress !== "function") return false;
|
|
483
|
+
onLongPress();
|
|
484
|
+
return true;
|
|
431
485
|
}
|
|
432
486
|
};
|
|
433
487
|
}
|
|
@@ -449,6 +503,19 @@ function wrapTouchable(fiber, label) {
|
|
|
449
503
|
}
|
|
450
504
|
};
|
|
451
505
|
}
|
|
506
|
+
var IDENTITY_MATCH_SCORE = 100;
|
|
507
|
+
function describeCandidate(fiber, kind) {
|
|
508
|
+
const props = fiber?.memoizedProps ?? {};
|
|
509
|
+
return props.testID ?? props.accessibilityLabel ?? props.placeholder ?? (kind === "target" ? findChildText(fiber) : void 0);
|
|
510
|
+
}
|
|
511
|
+
function isDescendantOf(fiber, ancestor) {
|
|
512
|
+
if (!fiber || !ancestor) return false;
|
|
513
|
+
let node = fiber.return;
|
|
514
|
+
for (let depth = 0; node && depth < 200; depth++, node = node.return) {
|
|
515
|
+
if (node === ancestor) return true;
|
|
516
|
+
}
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
452
519
|
function findInteractiveByIdentifier(query, options = {}) {
|
|
453
520
|
const root = getFiberRoot();
|
|
454
521
|
if (!root) {
|
|
@@ -460,6 +527,10 @@ function findInteractiveByIdentifier(query, options = {}) {
|
|
|
460
527
|
let bestScore = 0;
|
|
461
528
|
let bestHandle = null;
|
|
462
529
|
let bestId;
|
|
530
|
+
let bestFiber = null;
|
|
531
|
+
let bestNamed = false;
|
|
532
|
+
let bestDisabled = false;
|
|
533
|
+
let tied = [];
|
|
463
534
|
const visited = /* @__PURE__ */ new WeakSet();
|
|
464
535
|
const stack = [];
|
|
465
536
|
if (root.child) stack.push(root.child);
|
|
@@ -469,26 +540,52 @@ function findInteractiveByIdentifier(query, options = {}) {
|
|
|
469
540
|
visited.add(fiber);
|
|
470
541
|
if (fiber.sibling) stack.push(fiber.sibling);
|
|
471
542
|
if (shouldSkipSubtree2(fiber)) continue;
|
|
543
|
+
const props = fiber.memoizedProps ?? {};
|
|
472
544
|
let kind = null;
|
|
473
545
|
if (matchesName(fiber, "TextInput")) kind = "field";
|
|
474
|
-
else if (
|
|
546
|
+
else if (isSwitchLike(fiber, props)) kind = "toggle";
|
|
475
547
|
else if (isPressableTarget(fiber)) kind = "target";
|
|
476
548
|
if (kind && (!options.kind || options.kind === kind)) {
|
|
477
|
-
const
|
|
478
|
-
|
|
549
|
+
const detail = scoreMatchDetail(fiber, queryNorm, queryRaw);
|
|
550
|
+
const score = detail.score;
|
|
551
|
+
if (score > 0) {
|
|
552
|
+
if (score > bestScore) tied = [{ fiber, kind }];
|
|
553
|
+
else if (score === bestScore) tied.push({ fiber, kind });
|
|
554
|
+
}
|
|
555
|
+
const beatsBest = score > bestScore || score > 0 && score === bestScore && isDescendantOf(fiber, bestFiber);
|
|
556
|
+
if (beatsBest) {
|
|
479
557
|
bestScore = score;
|
|
480
|
-
|
|
481
|
-
|
|
558
|
+
bestFiber = fiber;
|
|
559
|
+
bestNamed = detail.named;
|
|
560
|
+
bestDisabled = detail.disabled;
|
|
561
|
+
bestId = describeCandidate(fiber, kind);
|
|
482
562
|
if (kind === "field") bestHandle = wrapTextInput(fiber);
|
|
483
563
|
else if (kind === "toggle") bestHandle = wrapSwitch(fiber);
|
|
484
564
|
else bestHandle = wrapTouchable(fiber, findChildText(fiber));
|
|
485
565
|
}
|
|
486
566
|
}
|
|
567
|
+
if (kind === "toggle") continue;
|
|
487
568
|
if (fiber.child) stack.push(fiber.child);
|
|
488
569
|
}
|
|
570
|
+
if (bestHandle && bestScore < (options.minScore ?? 0)) {
|
|
571
|
+
appilotsDebugLog(
|
|
572
|
+
`findInteractiveByIdentifier: query="${query}" \u2192 best match scored ${bestScore}, below the requested floor of ${options.minScore} \u2014 refusing it`
|
|
573
|
+
);
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
489
576
|
if (bestHandle) {
|
|
577
|
+
const independent = tied.filter(
|
|
578
|
+
(candidate) => !tied.some(
|
|
579
|
+
(other) => other.fiber !== candidate.fiber && isDescendantOf(other.fiber, candidate.fiber)
|
|
580
|
+
)
|
|
581
|
+
);
|
|
582
|
+
bestHandle.matchCount = independent.length;
|
|
583
|
+
bestHandle.named = bestNamed;
|
|
584
|
+
bestHandle.disabled = bestDisabled;
|
|
585
|
+
const candidates = independent.map(({ fiber, kind }) => describeCandidate(fiber, kind)).filter((id) => typeof id === "string" && id.length > 0).slice(0, 8);
|
|
586
|
+
if (candidates.length > 0) bestHandle.matchCandidates = candidates;
|
|
490
587
|
appilotsDebugLog(
|
|
491
|
-
`findInteractiveByIdentifier: query="${query}" \u2192 kind=${bestHandle.kind} label="${bestHandle.label ?? bestId}" score=${bestScore}`
|
|
588
|
+
`findInteractiveByIdentifier: query="${query}" \u2192 kind=${bestHandle.kind} label="${bestHandle.label ?? bestId}" score=${bestScore} named=${bestNamed} disabled=${bestDisabled} tied=${independent.length}`
|
|
492
589
|
);
|
|
493
590
|
} else {
|
|
494
591
|
appilotsDebugLog(`findInteractiveByIdentifier: query="${query}" \u2192 no match`);
|
|
@@ -898,11 +995,27 @@ function pressResolvedListItem(list, itemFiber, listIndex, itemIndex) {
|
|
|
898
995
|
const safeIdentifiableTouchables = identifiableTouchables.filter(
|
|
899
996
|
(candidate) => !touchableLooksDestructive(candidate)
|
|
900
997
|
);
|
|
998
|
+
const rowSwitches = findSwitchFibers(itemFiber);
|
|
999
|
+
if (rowSwitches.length > 0) {
|
|
1000
|
+
const switchCandidates = rowSwitches.map(
|
|
1001
|
+
(fiber, index) => describeTouchable(fiber) ?? `switch ${index + 1} of "list-${listIndex}-item-${itemIndex}" (role=toggle element)`
|
|
1002
|
+
);
|
|
1003
|
+
const pressCandidates = identifiableTouchables.map(describeTouchable).filter((d) => !!d);
|
|
1004
|
+
return {
|
|
1005
|
+
ok: false,
|
|
1006
|
+
reason: "ambiguous-touchables",
|
|
1007
|
+
refusal: "row-has-switch",
|
|
1008
|
+
error: `Item "list-${listIndex}-item-${itemIndex}" contains a switch, so pressing the row would activate the row itself and leave the switch untouched. To flip the switch, emit ui_interaction action="toggle" on the row's role=toggle element id from VISIBLE STATE` + (pressCandidates.length > 0 ? "; to activate the row instead, use a specific pressable id from the candidates." : "."),
|
|
1009
|
+
containerType: list.containerType,
|
|
1010
|
+
candidates: [...switchCandidates, ...pressCandidates].slice(0, 8)
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
901
1013
|
if (identifiableTouchables.length > 0 && safeIdentifiableTouchables.length === 0) {
|
|
902
1014
|
const candidates = identifiableTouchables.map(describeTouchable).filter((d) => !!d).slice(0, 8);
|
|
903
1015
|
return {
|
|
904
1016
|
ok: false,
|
|
905
1017
|
reason: "ambiguous-touchables",
|
|
1018
|
+
refusal: "destructive-only",
|
|
906
1019
|
error: `Item "list-${listIndex}-item-${itemIndex}" only exposes destructive controls. Use the specific button id from the row (for example the cancel button id) instead of pressing the list ordinal.`,
|
|
907
1020
|
containerType: list.containerType,
|
|
908
1021
|
candidates
|
|
@@ -1126,24 +1239,22 @@ function toggleListItemByIdentity(locator, options) {
|
|
|
1126
1239
|
const props = target?.memoizedProps ?? {};
|
|
1127
1240
|
const current = switchValue(props);
|
|
1128
1241
|
if (options?.expected !== void 0 && current === options.expected) {
|
|
1129
|
-
return { ok: true, containerType: list.containerType, value: current };
|
|
1242
|
+
return { ok: true, containerType: list.containerType, value: current, noop: true };
|
|
1130
1243
|
}
|
|
1131
|
-
const
|
|
1132
|
-
if (
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
onValueChange(!current);
|
|
1142
|
-
} catch (err) {
|
|
1244
|
+
const flipped = flipSwitchFiber(target, !current);
|
|
1245
|
+
if (!flipped.ok) {
|
|
1246
|
+
if (flipped.reason === "no-handler") {
|
|
1247
|
+
return {
|
|
1248
|
+
ok: false,
|
|
1249
|
+
reason: "no-touchable",
|
|
1250
|
+
error: `The row has a switch but it exposes no handler that could flip it (looked for ${SWITCH_FLIP_HANDLERS.join(", ")}).`,
|
|
1251
|
+
containerType: list.containerType
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1143
1254
|
return {
|
|
1144
1255
|
ok: false,
|
|
1145
1256
|
reason: "press-threw",
|
|
1146
|
-
error:
|
|
1257
|
+
error: flipped.error,
|
|
1147
1258
|
containerType: list.containerType
|
|
1148
1259
|
};
|
|
1149
1260
|
}
|
|
@@ -1171,6 +1282,9 @@ function screenDiagnose(targetId, category = "component-not-found") {
|
|
|
1171
1282
|
function isPressLike(action) {
|
|
1172
1283
|
return action === "press" || action === "tap" || action === "click" || action === "longPress";
|
|
1173
1284
|
}
|
|
1285
|
+
function isPlainPress(action) {
|
|
1286
|
+
return action === "press" || action === "tap" || action === "click";
|
|
1287
|
+
}
|
|
1174
1288
|
function isElementIdentifier(value) {
|
|
1175
1289
|
return value.trim().toLowerCase().startsWith("el:");
|
|
1176
1290
|
}
|
|
@@ -1281,6 +1395,54 @@ function dispatchRegisteredTarget(targetId, registry, action) {
|
|
|
1281
1395
|
diagnose: screenDiagnose(targetId)
|
|
1282
1396
|
};
|
|
1283
1397
|
}
|
|
1398
|
+
function dispatchIdentifiedToggle(targetId, action) {
|
|
1399
|
+
if (!isPressLike(action)) return null;
|
|
1400
|
+
const winner = findInteractiveByIdentifier(targetId, { minScore: IDENTITY_MATCH_SCORE });
|
|
1401
|
+
if (winner) {
|
|
1402
|
+
if (winner.kind !== "toggle" || !winner.setValue) return null;
|
|
1403
|
+
if ((winner.matchCount ?? 1) > 1) {
|
|
1404
|
+
return {
|
|
1405
|
+
success: false,
|
|
1406
|
+
error: `"${targetId}" names ${winner.matchCount} controls on this screen \u2014 aim at one of the candidates instead`,
|
|
1407
|
+
diagnose: {
|
|
1408
|
+
...screenDiagnose(targetId, "ambiguous-target"),
|
|
1409
|
+
recoverable: true,
|
|
1410
|
+
...winner.matchCandidates && winner.matchCandidates.length > 0 ? { candidates: winner.matchCandidates } : {}
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
if (!isPlainPress(action)) {
|
|
1415
|
+
if (winner.longPress?.()) return { success: true };
|
|
1416
|
+
return {
|
|
1417
|
+
success: false,
|
|
1418
|
+
error: `"${targetId}" names a switch and it declares no long-press handler. Use action="toggle" to flip it.`,
|
|
1419
|
+
diagnose: { ...screenDiagnose(targetId, "unknown"), recoverable: true }
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
appilotsDebugLog(
|
|
1423
|
+
`elementDispatch: "${action}" on "${targetId}" names a switch \u2014 flipping it instead of pressing, so the verb cannot land on an ancestor`
|
|
1424
|
+
);
|
|
1425
|
+
try {
|
|
1426
|
+
winner.setValue(!winner.getValue?.());
|
|
1427
|
+
return { success: true, effect: "changed" };
|
|
1428
|
+
} catch (err) {
|
|
1429
|
+
return {
|
|
1430
|
+
success: false,
|
|
1431
|
+
error: err?.message ?? `Could not flip the switch named "${targetId}"`,
|
|
1432
|
+
diagnose: screenDiagnose(targetId, "unknown")
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
const disabledToggle = findInteractiveByIdentifier(targetId, { kind: "toggle" });
|
|
1437
|
+
if (disabledToggle?.named && disabledToggle.disabled) {
|
|
1438
|
+
return {
|
|
1439
|
+
success: false,
|
|
1440
|
+
error: `The switch named "${targetId}" is currently disabled`,
|
|
1441
|
+
diagnose: screenDiagnose(targetId, "disabled")
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1284
1446
|
function dispatchFiberTarget(targetId, action) {
|
|
1285
1447
|
const kind = action === "toggle" ? "toggle" : action === "focus" ? "field" : "target";
|
|
1286
1448
|
const handle = findInteractiveByIdentifier(targetId, { kind });
|
|
@@ -1330,6 +1492,8 @@ function dispatchLegacyTarget(targetId, registry, action) {
|
|
|
1330
1492
|
}
|
|
1331
1493
|
const registered = dispatchRegisteredTarget(targetId, registry, action);
|
|
1332
1494
|
if (registered.success) return registered;
|
|
1495
|
+
const named = dispatchIdentifiedToggle(targetId, action);
|
|
1496
|
+
if (named) return named;
|
|
1333
1497
|
const fiber = dispatchFiberTarget(targetId, action);
|
|
1334
1498
|
if (fiber.success) return fiber;
|
|
1335
1499
|
return {
|
|
@@ -1346,6 +1510,9 @@ function dispatchElement(element, registry, action) {
|
|
|
1346
1510
|
diagnose: screenDiagnose(element.id, "disabled")
|
|
1347
1511
|
};
|
|
1348
1512
|
}
|
|
1513
|
+
if (element.role === "toggle" && (action === "toggle" || isPressLike(action))) {
|
|
1514
|
+
return dispatchToggleElement(element, registry);
|
|
1515
|
+
}
|
|
1349
1516
|
if (element.listContext && isPressLike(action)) {
|
|
1350
1517
|
const pressed = pressListItemByIdentity({
|
|
1351
1518
|
...element.listContext,
|
|
@@ -1354,6 +1521,18 @@ function dispatchElement(element, registry, action) {
|
|
|
1354
1521
|
texts: element.texts
|
|
1355
1522
|
});
|
|
1356
1523
|
if (pressed.ok) return { success: true };
|
|
1524
|
+
if (pressed.refusal) {
|
|
1525
|
+
return {
|
|
1526
|
+
success: false,
|
|
1527
|
+
error: pressed.error ?? `Could not press element "${element.id}" (${pressed.reason})`,
|
|
1528
|
+
diagnose: {
|
|
1529
|
+
...screenDiagnose(element.id),
|
|
1530
|
+
category: "ambiguous-target",
|
|
1531
|
+
recoverable: true,
|
|
1532
|
+
...pressed.candidates && pressed.candidates.length > 0 ? { candidates: pressed.candidates } : {}
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1357
1536
|
if (element.targetId && element.targetId !== element.id) {
|
|
1358
1537
|
const fallback = dispatchLegacyTarget(element.targetId, registry, action);
|
|
1359
1538
|
if (fallback.success) return fallback;
|
|
@@ -1385,50 +1564,6 @@ function dispatchElement(element, registry, action) {
|
|
|
1385
1564
|
diagnose: { ...screenDiagnose(element.id, "unknown"), recoverable: true }
|
|
1386
1565
|
};
|
|
1387
1566
|
}
|
|
1388
|
-
if (element.role === "toggle" && (action === "toggle" || isPressLike(action))) {
|
|
1389
|
-
if (element.listContext) {
|
|
1390
|
-
const toggled = toggleListItemByIdentity(
|
|
1391
|
-
{
|
|
1392
|
-
listIndex: element.listContext.listIndex,
|
|
1393
|
-
listId: element.listContext.listId,
|
|
1394
|
-
listLabel: element.listContext.listLabel,
|
|
1395
|
-
itemIndex: element.listContext.itemIndex,
|
|
1396
|
-
itemKey: element.listContext.itemKey,
|
|
1397
|
-
reactKey: element.listContext.reactKey,
|
|
1398
|
-
syntheticId: element.listContext.syntheticId,
|
|
1399
|
-
label: element.label,
|
|
1400
|
-
texts: element.texts
|
|
1401
|
-
},
|
|
1402
|
-
// The observation's `selected` is the state BEFORE this action.
|
|
1403
|
-
// Passing the desired end state makes the flip idempotent: a
|
|
1404
|
-
// retry or a recovery hop must not undo the work and call it
|
|
1405
|
-
// success. A toggle only knows how to invert, so without this the
|
|
1406
|
-
// second attempt silently returns the row to where it started.
|
|
1407
|
-
typeof element.selected === "boolean" ? { expected: !element.selected } : void 0
|
|
1408
|
-
);
|
|
1409
|
-
if (toggled.ok) {
|
|
1410
|
-
return {
|
|
1411
|
-
success: true,
|
|
1412
|
-
// Grounded: read back off the fiber after the handler ran, not
|
|
1413
|
-
// inferred from "the call did not throw".
|
|
1414
|
-
effect: "changed"
|
|
1415
|
-
};
|
|
1416
|
-
}
|
|
1417
|
-
return {
|
|
1418
|
-
success: false,
|
|
1419
|
-
error: toggled.error ?? `Could not flip the switch in row "${element.label ?? element.id}" (${toggled.reason})`,
|
|
1420
|
-
diagnose: {
|
|
1421
|
-
...screenDiagnose(
|
|
1422
|
-
element.id,
|
|
1423
|
-
toggled.reason === "ambiguous-touchables" || toggled.reason === "ambiguous-items" ? "ambiguous-target" : "component-not-found"
|
|
1424
|
-
),
|
|
1425
|
-
...toggled.candidates && toggled.candidates.length > 0 ? { candidates: toggled.candidates } : {}
|
|
1426
|
-
}
|
|
1427
|
-
};
|
|
1428
|
-
}
|
|
1429
|
-
const targetId = element.targetId ?? element.label ?? element.id;
|
|
1430
|
-
return dispatchLegacyTarget(targetId, registry, "toggle");
|
|
1431
|
-
}
|
|
1432
1567
|
if (element.role === "input" && (action === "focus" || isPressLike(action))) {
|
|
1433
1568
|
const targetId = element.targetId ?? element.label ?? element.id;
|
|
1434
1569
|
return dispatchLegacyTarget(targetId, registry, "focus");
|
|
@@ -1443,6 +1578,54 @@ function dispatchElement(element, registry, action) {
|
|
|
1443
1578
|
diagnose: screenDiagnose(element.id, "unknown")
|
|
1444
1579
|
};
|
|
1445
1580
|
}
|
|
1581
|
+
function dispatchToggleElement(element, registry) {
|
|
1582
|
+
if (element.listContext) {
|
|
1583
|
+
const toggled = toggleListItemByIdentity(
|
|
1584
|
+
{
|
|
1585
|
+
listIndex: element.listContext.listIndex,
|
|
1586
|
+
listId: element.listContext.listId,
|
|
1587
|
+
listLabel: element.listContext.listLabel,
|
|
1588
|
+
itemIndex: element.listContext.itemIndex,
|
|
1589
|
+
itemKey: element.listContext.itemKey,
|
|
1590
|
+
reactKey: element.listContext.reactKey,
|
|
1591
|
+
syntheticId: element.listContext.syntheticId,
|
|
1592
|
+
label: element.label,
|
|
1593
|
+
texts: element.texts
|
|
1594
|
+
},
|
|
1595
|
+
// The observation's `selected` is the state BEFORE this action.
|
|
1596
|
+
// Passing the desired end state makes the flip idempotent: a
|
|
1597
|
+
// retry or a recovery hop must not undo the work and call it
|
|
1598
|
+
// success. A toggle only knows how to invert, so without this the
|
|
1599
|
+
// second attempt silently returns the row to where it started.
|
|
1600
|
+
typeof element.selected === "boolean" ? { expected: !element.selected } : void 0
|
|
1601
|
+
);
|
|
1602
|
+
if (toggled.ok) {
|
|
1603
|
+
return {
|
|
1604
|
+
success: true,
|
|
1605
|
+
// Honest split (#504): a real flip is 'changed'; the idempotent
|
|
1606
|
+
// no-op — the switch already held the requested state, nothing
|
|
1607
|
+
// was touched — is 'completed' (goal state verified), NEVER
|
|
1608
|
+
// 'changed' (nothing changed this hop) and NEVER 'none' (which
|
|
1609
|
+
// would send recovery re-flipping a switch that is already
|
|
1610
|
+
// right).
|
|
1611
|
+
effect: toggled.noop ? "completed" : "changed"
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
return {
|
|
1615
|
+
success: false,
|
|
1616
|
+
error: toggled.error ?? `Could not flip the switch in row "${element.label ?? element.id}" (${toggled.reason})`,
|
|
1617
|
+
diagnose: {
|
|
1618
|
+
...screenDiagnose(
|
|
1619
|
+
element.id,
|
|
1620
|
+
toggled.reason === "ambiguous-touchables" || toggled.reason === "ambiguous-items" ? "ambiguous-target" : "component-not-found"
|
|
1621
|
+
),
|
|
1622
|
+
...toggled.candidates && toggled.candidates.length > 0 ? { candidates: toggled.candidates } : {}
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
const targetId = element.targetId ?? element.label ?? element.id;
|
|
1627
|
+
return dispatchLegacyTarget(targetId, registry, "toggle");
|
|
1628
|
+
}
|
|
1446
1629
|
function dispatchStructuredElementId(identifier, action) {
|
|
1447
1630
|
if (!isElementIdentifier(identifier) || !isPressLike(action)) return null;
|
|
1448
1631
|
const parsed = parseStableElementId(identifier);
|
|
@@ -1469,6 +1652,8 @@ function dispatchElementOrTarget(identifier, registry, action = "press") {
|
|
|
1469
1652
|
if (result.success) return result;
|
|
1470
1653
|
if (result.diagnose?.category === "disabled") return result;
|
|
1471
1654
|
if (element.role === "slider") return result;
|
|
1655
|
+
if (element.role === "toggle") return result;
|
|
1656
|
+
if (result.diagnose?.category === "ambiguous-target") return result;
|
|
1472
1657
|
}
|
|
1473
1658
|
const structured = dispatchStructuredElementId(identifier, action);
|
|
1474
1659
|
if (structured?.success) return structured;
|
|
@@ -2207,7 +2392,6 @@ function uiInteractionHandler(payload, context) {
|
|
|
2207
2392
|
appilotsDebugLog(
|
|
2208
2393
|
`uiInteractionHandler: interaction element dispatch succeeded for "${rawComponentId}"`
|
|
2209
2394
|
);
|
|
2210
|
-
return { success: true };
|
|
2211
2395
|
}
|
|
2212
2396
|
return result;
|
|
2213
2397
|
}
|
|
@@ -2239,12 +2423,12 @@ function uiInteractionHandler(payload, context) {
|
|
|
2239
2423
|
};
|
|
2240
2424
|
}
|
|
2241
2425
|
if (result.reason === "ambiguous-touchables") {
|
|
2242
|
-
const
|
|
2426
|
+
const namesConcreteChoice = result.refusal !== void 0 || /only exposes destructive controls|contains a switch/i.test(result.error ?? "");
|
|
2243
2427
|
return {
|
|
2244
2428
|
success: false,
|
|
2245
2429
|
error: result.error ?? `Synthetic list ordinal press failed (${result.reason ?? "unknown"})`,
|
|
2246
2430
|
diagnose: {
|
|
2247
|
-
category:
|
|
2431
|
+
category: namesConcreteChoice ? "ambiguous-target" : "unknown",
|
|
2248
2432
|
targetId: rawComponentId,
|
|
2249
2433
|
screen,
|
|
2250
2434
|
recoverable: true,
|
|
@@ -2320,6 +2504,8 @@ function uiInteractionHandler(payload, context) {
|
|
|
2320
2504
|
appilotsDebugLog(
|
|
2321
2505
|
`uiInteractionHandler: registry miss for "${componentId}", falling back to fiber tree`
|
|
2322
2506
|
);
|
|
2507
|
+
const namedToggle = dispatchIdentifiedToggle(componentId, actionStr);
|
|
2508
|
+
if (namedToggle) return namedToggle;
|
|
2323
2509
|
const handle = findInteractiveByIdentifier(componentId, {
|
|
2324
2510
|
kind: actionStr === "toggle" ? "toggle" : actionStr === "focus" ? "field" : "target"
|
|
2325
2511
|
});
|
|
@@ -2332,10 +2518,6 @@ function uiInteractionHandler(payload, context) {
|
|
|
2332
2518
|
);
|
|
2333
2519
|
return { success: true };
|
|
2334
2520
|
}
|
|
2335
|
-
if (actionStr === "press" && handle.kind === "toggle" && handle.setValue) {
|
|
2336
|
-
handle.setValue(!handle.getValue?.());
|
|
2337
|
-
return { success: true };
|
|
2338
|
-
}
|
|
2339
2521
|
if (actionStr === "toggle" && handle.kind === "toggle" && handle.setValue) {
|
|
2340
2522
|
handle.setValue(!handle.getValue?.());
|
|
2341
2523
|
return { success: true };
|
|
@@ -3217,6 +3399,14 @@ async function formFillHandler(payload, context) {
|
|
|
3217
3399
|
allSuccess = false;
|
|
3218
3400
|
}
|
|
3219
3401
|
if (payload.submitAfterFill === true) {
|
|
3402
|
+
if (!permissions.canSubmitForms) {
|
|
3403
|
+
return {
|
|
3404
|
+
success: false,
|
|
3405
|
+
error: "Form submission is not permitted",
|
|
3406
|
+
fieldResults,
|
|
3407
|
+
diagnose: { category: "unknown", screen: screenPath }
|
|
3408
|
+
};
|
|
3409
|
+
}
|
|
3220
3410
|
if (!allSuccess) {
|
|
3221
3411
|
const failedIds = fieldResults.filter((result) => !result.success).map((result) => result.fieldId);
|
|
3222
3412
|
appilotsDebugWarn(
|
|
@@ -3224,6 +3414,9 @@ async function formFillHandler(payload, context) {
|
|
|
3224
3414
|
);
|
|
3225
3415
|
} else {
|
|
3226
3416
|
await sleep2(120);
|
|
3417
|
+
if (!permissions.canSubmitForms) {
|
|
3418
|
+
return { success: false, error: "Form submission is not permitted", fieldResults };
|
|
3419
|
+
}
|
|
3227
3420
|
const submitResult = pressSubmitButton(
|
|
3228
3421
|
{
|
|
3229
3422
|
screen,
|
|
@@ -3529,6 +3722,59 @@ async function confirmHandler(_actionId, _payload) {
|
|
|
3529
3722
|
return { success: true, effect: "unknown" };
|
|
3530
3723
|
}
|
|
3531
3724
|
|
|
3725
|
+
// src/platform/asyncBudget.ts
|
|
3726
|
+
var POST_ACTION_LOADING_MAX_MS = 6e3;
|
|
3727
|
+
var INFERRED_ASYNC_LOADING_MAX_MS = 1e4;
|
|
3728
|
+
var POST_NAVIGATE_LOADING_MAX_MS = 3e3;
|
|
3729
|
+
var MAX_DECLARED_ASYNC_BUDGET_MS = 12e4;
|
|
3730
|
+
var ASYNC_BUSY_CEILING_MS = MAX_DECLARED_ASYNC_BUDGET_MS;
|
|
3731
|
+
function declaredBudgetMs(hints) {
|
|
3732
|
+
const declared = hints?.asyncBudgetMs;
|
|
3733
|
+
if (typeof declared !== "number" || !Number.isFinite(declared) || declared <= 0) return void 0;
|
|
3734
|
+
return Math.min(declared, MAX_DECLARED_ASYNC_BUDGET_MS);
|
|
3735
|
+
}
|
|
3736
|
+
function resolveSettleCapMs(hints) {
|
|
3737
|
+
const declared = declaredBudgetMs(hints);
|
|
3738
|
+
if (declared !== void 0) return declared;
|
|
3739
|
+
return hints?.isAsyncTrigger === true ? INFERRED_ASYNC_LOADING_MAX_MS : POST_ACTION_LOADING_MAX_MS;
|
|
3740
|
+
}
|
|
3741
|
+
function resolveBusyCeilingMs(hints) {
|
|
3742
|
+
return declaredBudgetMs(hints) === void 0 ? ASYNC_BUSY_CEILING_MS : void 0;
|
|
3743
|
+
}
|
|
3744
|
+
function resolvePostNavigateCapMs(hints) {
|
|
3745
|
+
return Math.max(POST_NAVIGATE_LOADING_MAX_MS, declaredBudgetMs(hints) ?? 0);
|
|
3746
|
+
}
|
|
3747
|
+
function resolveActionHints(screen, pressedComponentId, actionPayload) {
|
|
3748
|
+
if (actionPayload && typeof actionPayload === "object") {
|
|
3749
|
+
const payload = actionPayload;
|
|
3750
|
+
const inline = payload._appilotsInferred;
|
|
3751
|
+
const declared = payload._appilotsAsyncBudgetMs;
|
|
3752
|
+
if (inline && typeof inline === "object") {
|
|
3753
|
+
const hints = { ...inline };
|
|
3754
|
+
if (typeof declared === "number") hints.asyncBudgetMs = declared;
|
|
3755
|
+
return hints;
|
|
3756
|
+
}
|
|
3757
|
+
if (typeof declared === "number") return { asyncBudgetMs: declared };
|
|
3758
|
+
}
|
|
3759
|
+
if (!screen || !pressedComponentId) return void 0;
|
|
3760
|
+
const meta = getScreenMetadata(screen);
|
|
3761
|
+
const actions = meta?.actions;
|
|
3762
|
+
if (!actions || actions.length === 0) return void 0;
|
|
3763
|
+
for (const a of actions) {
|
|
3764
|
+
if (typeof a === "string") continue;
|
|
3765
|
+
if (idLooselyMatches(a.id, pressedComponentId)) {
|
|
3766
|
+
const inferred = a.appilotsInferred;
|
|
3767
|
+
const declared = a.asyncBudgetMs;
|
|
3768
|
+
const hints = {
|
|
3769
|
+
...inferred && typeof inferred === "object" ? inferred : {},
|
|
3770
|
+
...typeof declared === "number" ? { asyncBudgetMs: declared } : {}
|
|
3771
|
+
};
|
|
3772
|
+
return Object.keys(hints).length > 0 ? hints : void 0;
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
return void 0;
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3532
3778
|
// src/executor/postInteractionSettle.ts
|
|
3533
3779
|
var sleepMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3534
3780
|
function targetIdFromPayload(payload) {
|
|
@@ -3577,7 +3823,8 @@ async function waitForPostInteractionSettle(payload) {
|
|
|
3577
3823
|
if (discreteFieldSignature() !== baselineFields) return { effect: "changed" };
|
|
3578
3824
|
return { effect: settled.fingerprint !== baseline.fingerprint ? "changed" : "none" };
|
|
3579
3825
|
};
|
|
3580
|
-
const
|
|
3826
|
+
const declared = declaredBudgetMs(resolveActionHints(fromScreen, targetId || null, payload));
|
|
3827
|
+
const capMs = declared ?? (isListItemPressTarget(targetId) ? 5e3 : 1e4);
|
|
3581
3828
|
if (modalOrLoading || fingerprintChanged) {
|
|
3582
3829
|
await waitForLoadingSettle({
|
|
3583
3830
|
pressedComponentId: targetId || null,
|
|
@@ -3874,7 +4121,7 @@ async function runWithConfirmedDestructiveContext(enabled, host, fn) {
|
|
|
3874
4121
|
}
|
|
3875
4122
|
|
|
3876
4123
|
// src/platform/reactNativeAdapter.ts
|
|
3877
|
-
var
|
|
4124
|
+
var deltaTracker = createSnapshotDeltaTracker();
|
|
3878
4125
|
function resolveScreenMetadata() {
|
|
3879
4126
|
const activePath = getActiveRouteNames();
|
|
3880
4127
|
for (let i = activePath.length - 1; i >= 0; i--) {
|
|
@@ -3888,6 +4135,7 @@ function resolveScreenMetadata() {
|
|
|
3888
4135
|
return screen ? getScreenMetadata(screen) : void 0;
|
|
3889
4136
|
}
|
|
3890
4137
|
function buildAgentContext(extras = {}) {
|
|
4138
|
+
if (extras.newMission === true) deltaTracker.reset();
|
|
3891
4139
|
const context = {};
|
|
3892
4140
|
context.platform = "react-native";
|
|
3893
4141
|
const screen = getCurrentScreen();
|
|
@@ -3897,7 +4145,10 @@ function buildAgentContext(extras = {}) {
|
|
|
3897
4145
|
const screenMeta = resolveScreenMetadata();
|
|
3898
4146
|
if (screenMeta) context.screenMetadata = screenMeta;
|
|
3899
4147
|
try {
|
|
3900
|
-
|
|
4148
|
+
const snapshot = captureSnapshot();
|
|
4149
|
+
const delta = deltaTracker.observe(snapshot);
|
|
4150
|
+
if (delta) snapshot.delta = delta;
|
|
4151
|
+
context.snapshot = snapshot;
|
|
3901
4152
|
} catch (err) {
|
|
3902
4153
|
appilotsDebugWarn("buildAgentContext: captureSnapshot failed", err);
|
|
3903
4154
|
}
|
|
@@ -3913,33 +4164,21 @@ function buildAgentContext(extras = {}) {
|
|
|
3913
4164
|
if (extras.loadingPending === true) context.loadingPending = true;
|
|
3914
4165
|
return context;
|
|
3915
4166
|
}
|
|
3916
|
-
function resolveActionHints(screen, pressedComponentId, actionPayload) {
|
|
3917
|
-
if (actionPayload && typeof actionPayload === "object") {
|
|
3918
|
-
const inline = actionPayload._appilotsInferred;
|
|
3919
|
-
if (inline && typeof inline === "object") {
|
|
3920
|
-
return inline;
|
|
3921
|
-
}
|
|
3922
|
-
}
|
|
3923
|
-
if (!screen || !pressedComponentId) return void 0;
|
|
3924
|
-
const meta = getScreenMetadata(screen);
|
|
3925
|
-
const actions = meta?.actions;
|
|
3926
|
-
if (!actions || actions.length === 0) return void 0;
|
|
3927
|
-
for (const a of actions) {
|
|
3928
|
-
if (typeof a === "string") continue;
|
|
3929
|
-
if (idLooselyMatches(a.id, pressedComponentId)) {
|
|
3930
|
-
const inferred = a.appilotsInferred;
|
|
3931
|
-
if (inferred && typeof inferred === "object") return inferred;
|
|
3932
|
-
return void 0;
|
|
3933
|
-
}
|
|
3934
|
-
}
|
|
3935
|
-
return void 0;
|
|
3936
|
-
}
|
|
3937
4167
|
async function settleTurn(input) {
|
|
3938
4168
|
const toolWithoutEffectIds = /* @__PURE__ */ new Set();
|
|
3939
4169
|
let loadingPending = false;
|
|
3940
4170
|
const { turnActions, pressedComponentId: pressedId, navigateTargetScreen } = input;
|
|
3941
4171
|
const successById = new Map(input.results.map((r) => [r.actionId, r.success]));
|
|
3942
4172
|
const typeById = new Map(input.results.map((r) => [r.actionId, r.type]));
|
|
4173
|
+
const pressedAction = turnActions.find(
|
|
4174
|
+
(a) => a.type === "ui_interaction" && a.payload?.componentId === pressedId
|
|
4175
|
+
);
|
|
4176
|
+
const hints = resolveActionHints(
|
|
4177
|
+
getCurrentScreen(),
|
|
4178
|
+
pressedId,
|
|
4179
|
+
pressedAction?.payload
|
|
4180
|
+
);
|
|
4181
|
+
const capSource = typeof hints?.asyncBudgetMs === "number" && hints.asyncBudgetMs > 0 ? " (app-declared)" : hints?.isAsyncTrigger ? " (mcp-inferred)" : "";
|
|
3943
4182
|
if (input.hadNavigate) {
|
|
3944
4183
|
const settle = await waitForScreenSettle({
|
|
3945
4184
|
expectingChange: true,
|
|
@@ -3960,27 +4199,26 @@ async function settleTurn(input) {
|
|
|
3960
4199
|
if (type === "navigate") toolWithoutEffectIds.add(actionId);
|
|
3961
4200
|
}
|
|
3962
4201
|
}
|
|
4202
|
+
const postNavigateCapMs = resolvePostNavigateCapMs(hints);
|
|
3963
4203
|
const loadSettle = await waitForLoadingSettle({
|
|
3964
4204
|
fromScreen: settle.screen ?? null,
|
|
3965
|
-
maxMs:
|
|
4205
|
+
maxMs: postNavigateCapMs
|
|
3966
4206
|
});
|
|
3967
4207
|
appilotsDebugLog(
|
|
3968
|
-
`settleTurn: post-navigate loading settle \u2014 settled=${loadSettle.settled} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs}`
|
|
4208
|
+
`settleTurn: post-navigate loading settle \u2014 settled=${loadSettle.settled} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs} cap=${postNavigateCapMs}${capSource}`
|
|
3969
4209
|
);
|
|
3970
4210
|
if (loadSettle.loadingPending) loadingPending = true;
|
|
3971
4211
|
} else {
|
|
3972
|
-
const
|
|
3973
|
-
|
|
3974
|
-
);
|
|
3975
|
-
const hints = resolveActionHints(getCurrentScreen(), pressedId, pressedAction?.payload);
|
|
3976
|
-
const settleCapMs = hints?.isAsyncTrigger === true ? 1e4 : POST_ACTION_LOADING_MAX_MS;
|
|
4212
|
+
const settleCapMs = resolveSettleCapMs(hints);
|
|
4213
|
+
const busyMaxMs = resolveBusyCeilingMs(hints);
|
|
3977
4214
|
const loadSettle = await waitForLoadingSettle({
|
|
3978
4215
|
pressedComponentId: pressedId,
|
|
3979
4216
|
fromScreen: getCurrentScreen(),
|
|
3980
|
-
maxMs: settleCapMs
|
|
4217
|
+
maxMs: settleCapMs,
|
|
4218
|
+
...busyMaxMs !== void 0 ? { busyMaxMs } : {}
|
|
3981
4219
|
});
|
|
3982
4220
|
appilotsDebugLog(
|
|
3983
|
-
`settleTurn: post-action loading settle \u2014 pressedId="${pressedId ?? ""}" settled=${loadSettle.settled} transitioned=${loadSettle.transitioned} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs} cap=${settleCapMs}${
|
|
4221
|
+
`settleTurn: post-action loading settle \u2014 pressedId="${pressedId ?? ""}" settled=${loadSettle.settled} transitioned=${loadSettle.transitioned} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs} cap=${settleCapMs}${capSource}${busyMaxMs !== void 0 ? ` busyCap=${busyMaxMs}` : ""}`
|
|
3984
4222
|
);
|
|
3985
4223
|
if (loadSettle.loadingPending) loadingPending = true;
|
|
3986
4224
|
const listPressActions = turnActions.filter((action) => {
|