@appilots/sdk 0.11.0 → 0.11.1

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "sourceHash": "528f6f08e4f5dfc0",
3
- "gitSha": "a86ce80f621e951810622d1b247d355f68904451",
4
- "builtAt": "2026-08-25T10:30:07.418Z"
2
+ "sourceHash": "6574aab0f4f8f43f",
3
+ "gitSha": "cdbfa225c28ac0cf0bf495bb8a8ec0c55d4dc1d1",
4
+ "builtAt": "2026-08-26T01:42:57.571Z"
5
5
  }
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunk55Y2AQE7_js = require('./chunk-55Y2AQE7.js');
3
+ var chunkKZ2BLMS4_js = require('./chunk-KZ2BLMS4.js');
4
4
  var React = require('react');
5
5
 
6
6
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -15,17 +15,17 @@ function getActiveRouteName(state) {
15
15
  return route.name;
16
16
  }
17
17
  function AppilotsNavigationContainer({ children }) {
18
- const { emit } = chunk55Y2AQE7_js.useAppilotsContext();
18
+ const { emit } = chunkKZ2BLMS4_js.useAppilotsContext();
19
19
  const internalRef = React.useRef(null);
20
20
  const handleRef = React.useCallback(
21
21
  (instance) => {
22
22
  internalRef.current = instance;
23
- chunk55Y2AQE7_js.setNavigationRef(instance);
23
+ chunkKZ2BLMS4_js.setNavigationRef(instance);
24
24
  const childRef = children.ref;
25
25
  if (typeof childRef === "function") childRef(instance);
26
26
  else if (childRef && typeof childRef === "object") childRef.current = instance;
27
27
  if (instance) {
28
- chunk55Y2AQE7_js.appilotsDebugLog("NavigationContainer ref captured");
28
+ chunkKZ2BLMS4_js.appilotsDebugLog("NavigationContainer ref captured");
29
29
  }
30
30
  },
31
31
  [children]
@@ -34,8 +34,8 @@ function AppilotsNavigationContainer({ children }) {
34
34
  (state) => {
35
35
  const routeName = getActiveRouteName(state);
36
36
  if (routeName) {
37
- chunk55Y2AQE7_js.appilotsDebugLog(`Screen changed \u2192 "${routeName}"`);
38
- chunk55Y2AQE7_js.setCurrentScreen(routeName);
37
+ chunkKZ2BLMS4_js.appilotsDebugLog(`Screen changed \u2192 "${routeName}"`);
38
+ chunkKZ2BLMS4_js.setCurrentScreen(routeName);
39
39
  emit({
40
40
  type: "navigation:change",
41
41
  timestamp: Date.now(),
@@ -50,8 +50,8 @@ function AppilotsNavigationContainer({ children }) {
50
50
  const timer = setTimeout(() => {
51
51
  const route = internalRef.current?.getCurrentRoute?.();
52
52
  if (route?.name) {
53
- chunk55Y2AQE7_js.appilotsDebugLog(`Initial screen \u2192 "${route.name}"`);
54
- chunk55Y2AQE7_js.setCurrentScreen(route.name);
53
+ chunkKZ2BLMS4_js.appilotsDebugLog(`Initial screen \u2192 "${route.name}"`);
54
+ chunkKZ2BLMS4_js.setCurrentScreen(route.name);
55
55
  emit({
56
56
  type: "navigation:change",
57
57
  timestamp: Date.now(),
@@ -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, idLooselyMatches, parseStableElementId, shouldSkipSubtree, isAlwaysListContainer, isHeuristicListContainer, findListItems, getTypeName, foldForCompare, isModalFiber, getAllScreens, snapshotShowsOpenCreateForm, switchValue, isReferenceable, looksDestructiveActionLabel, isGenericSelectValue, FORM_WRITE_SUCCESS_TEXT_RE, isSwitchLike, TOUCHABLE_NAMES } from './chunk-G6KI7KVH.mjs';
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, isSwitchLike, normalize3, matchScore, FORM_VALIDATION_TEXT_RE, idLooselyMatches, parseStableElementId, shouldSkipSubtree, isAlwaysListContainer, isHeuristicListContainer, findListItems, getTypeName, foldForCompare, isModalFiber, switchValue, getAllScreens, snapshotShowsOpenCreateForm, isReferenceable, looksDestructiveActionLabel, isGenericSelectValue, FORM_WRITE_SUCCESS_TEXT_RE, TOUCHABLE_NAMES } from './chunk-PQZ64CCE.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 scoreMatch(fiber, queryNorm, queryRaw) {
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
- if (best > 0 && props.disabled === true) {
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 && typeof props.accessibilityState === "object" && props.accessibilityState !== null && props.accessibilityState.disabled === true) {
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
- getValue: () => !!getProps().value,
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
- if (typeof onValueChange === "function") onValueChange(boolVal);
429
- else if (typeof onChange === "function") onChange({ nativeEvent: { value: boolVal } });
430
- else throw new Error("Switch has no onValueChange/onChange handler");
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 (matchesName(fiber, "Switch")) kind = "toggle";
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 score = scoreMatch(fiber, queryNorm, queryRaw);
478
- if (score > bestScore) {
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
- const props = fiber.memoizedProps ?? {};
481
- bestId = props.testID ?? props.accessibilityLabel ?? props.placeholder ?? (kind === "target" ? findChildText(fiber) : void 0);
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 };
1130
- }
1131
- const onValueChange = props.onValueChange ?? props.onChange;
1132
- if (typeof onValueChange !== "function") {
1133
- return {
1134
- ok: false,
1135
- reason: "no-touchable",
1136
- error: "The row has a switch but it exposes no onValueChange handler.",
1137
- containerType: list.containerType
1138
- };
1242
+ return { ok: true, containerType: list.containerType, value: current, noop: true };
1139
1243
  }
1140
- try {
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: err?.message ?? "switch handler threw",
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 isDestructiveOnly = /only exposes destructive controls/i.test(result.error ?? "");
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: isDestructiveOnly ? "ambiguous-target" : "unknown",
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 };
@@ -4594,6 +4594,8 @@ function isSwitchLike(fiber, props) {
4594
4594
  }
4595
4595
  function switchValue(props) {
4596
4596
  if (typeof props?.value === "boolean") return props.value;
4597
+ const aria = props?.["aria-checked"];
4598
+ if (typeof aria === "boolean") return aria;
4597
4599
  const checked = props?.accessibilityState?.checked;
4598
4600
  return checked === true;
4599
4601
  }
@@ -5837,7 +5839,7 @@ async function waitForLoadingSettle(opts = {}) {
5837
5839
  }
5838
5840
 
5839
5841
  // src/version.ts
5840
- var SDK_VERSION2 = "0.11.0";
5842
+ var SDK_VERSION2 = "0.11.1";
5841
5843
  var FiberSentinel = class extends React2__default.default.Component {
5842
5844
  componentDidMount() {
5843
5845
  const fiber = this._reactInternals ?? this._reactInternalFiber ?? null;
@@ -1,4 +1,4 @@
1
- import { useAppilotsContext, setNavigationRef, appilotsDebugLog, setCurrentScreen } from './chunk-G6KI7KVH.mjs';
1
+ import { useAppilotsContext, setNavigationRef, appilotsDebugLog, setCurrentScreen } from './chunk-PQZ64CCE.mjs';
2
2
  import React, { useRef, useCallback, useEffect } from 'react';
3
3
 
4
4
  function getActiveRouteName(state) {
@@ -4588,6 +4588,8 @@ function isSwitchLike(fiber, props) {
4588
4588
  }
4589
4589
  function switchValue(props) {
4590
4590
  if (typeof props?.value === "boolean") return props.value;
4591
+ const aria = props?.["aria-checked"];
4592
+ if (typeof aria === "boolean") return aria;
4591
4593
  const checked = props?.accessibilityState?.checked;
4592
4594
  return checked === true;
4593
4595
  }
@@ -5831,7 +5833,7 @@ async function waitForLoadingSettle(opts = {}) {
5831
5833
  }
5832
5834
 
5833
5835
  // src/version.ts
5834
- var SDK_VERSION2 = "0.11.0";
5836
+ var SDK_VERSION2 = "0.11.1";
5835
5837
  var FiberSentinel = class extends React2.Component {
5836
5838
  componentDidMount() {
5837
5839
  const fiber = this._reactInternals ?? this._reactInternalFiber ?? null;