peachsim 0.1.1620 → 0.1.1635

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +45 -6
  3. package/cli/app-config.ts +4 -1
  4. package/cli/app-fonts.ts +39 -18
  5. package/cli/app-stubs.ts +224 -9
  6. package/cli/bridge-flow-runner.ts +248 -156
  7. package/cli/cloud-client.ts +68 -57
  8. package/cli/command-registry.ts +1 -0
  9. package/cli/commands/box/search-command.ts +3 -2
  10. package/cli/commands/box.ts +10 -29
  11. package/cli/commands/control.ts +391 -92
  12. package/cli/commands/doctor.ts +17 -184
  13. package/cli/commands/flow.ts +79 -11
  14. package/cli/commands/inspect/core.ts +10 -5
  15. package/cli/commands/inspect/settling.ts +71 -22
  16. package/cli/commands/inspect/wait-ready.ts +12 -0
  17. package/cli/commands/inspect.ts +26 -3
  18. package/cli/commands/install-desktop.ts +1 -0
  19. package/cli/commands/maestro-compare.ts +105 -67
  20. package/cli/commands/maestro.ts +9 -1
  21. package/cli/commands/platform.ts +71 -36
  22. package/cli/commands/record.ts +21 -1
  23. package/cli/commands/screenshot.ts +23 -1
  24. package/cli/commands/serve.ts +7 -2
  25. package/cli/commands/setup.ts +19 -42
  26. package/cli/commands/swift.ts +5 -0
  27. package/cli/commands/upload.ts +51 -8
  28. package/cli/desktop-companion.ts +102 -17
  29. package/cli/dev-build-freshness.ts +37 -2
  30. package/cli/drivers/desktop.ts +14 -1
  31. package/cli/drivers/playwright-sim-host.ts +44 -8
  32. package/cli/drivers/playwright.ts +9 -8
  33. package/cli/drivers/types.ts +5 -0
  34. package/cli/maestro-accessibility.ts +53 -0
  35. package/cli/maestro-compare.ts +108 -12
  36. package/cli/main.ts +2 -1
  37. package/cli/outbound-endpoints.ts +35 -6
  38. package/cli/parent-pid.ts +0 -9
  39. package/cli/pause-aware-timeout.ts +102 -0
  40. package/cli/remote-simulators.ts +112 -0
  41. package/cli/remoteSimulatorContract.ts +21 -0
  42. package/cli/scene-video.ts +89 -0
  43. package/cli/ws-bridge.ts +23 -14
  44. package/detox/attempt-events.cjs +4 -0
  45. package/detox/expectations.ts +5 -1
  46. package/detox/index.ts +241 -181
  47. package/detox/motion-change.cjs +73 -0
  48. package/detox/motion-change.d.cts +21 -0
  49. package/detox/navigation-lifecycle.ts +22 -4
  50. package/dist-lib/agent-daemon-client.cjs +3 -2
  51. package/dist-lib/agent-events.cjs +1 -1
  52. package/dist-lib/attached-projects.cjs +1 -1
  53. package/dist-lib/auth/shared-session.cjs +3 -2
  54. package/dist-lib/backend-origin.cjs +1 -1
  55. package/dist-lib/beta.cjs +1 -1
  56. package/dist-lib/beta.mjs +1 -1
  57. package/dist-lib/bridge-constants.cjs +13 -1
  58. package/dist-lib/bridge-contract-input.cjs +1 -1
  59. package/dist-lib/bridge-contract-input.mjs +1 -1
  60. package/dist-lib/bridge-contract.cjs +14 -8
  61. package/dist-lib/bridge-contract.mjs +13 -7
  62. package/dist-lib/bundle-channel.cjs +10480 -7962
  63. package/dist-lib/capture-contract.cjs +1 -1
  64. package/dist-lib/capture-contract.mjs +1 -1
  65. package/dist-lib/cloud-contract.cjs +1 -1
  66. package/dist-lib/cloud-contract.mjs +1 -1
  67. package/dist-lib/cloud-test-args.cjs +1 -1
  68. package/dist-lib/cloud-test-args.mjs +1 -1
  69. package/dist-lib/cloud.cjs +3 -2
  70. package/dist-lib/cloud.mjs +3 -2
  71. package/dist-lib/config.cjs +10 -6
  72. package/dist-lib/config.mjs +10 -6
  73. package/dist-lib/detox/index.cjs +1854 -966
  74. package/dist-lib/dev-bundle-resolution.cjs +64 -24
  75. package/dist-lib/expo-swift-bundle.cjs +8715 -6271
  76. package/dist-lib/expo-swift-metadata.cjs +7 -4
  77. package/dist-lib/expo-swift-metadata.mjs +7 -4
  78. package/dist-lib/home-paths.cjs +32 -3
  79. package/dist-lib/host/bridge-host.cjs +10135 -7060
  80. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  81. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  82. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  83. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  84. package/dist-lib/host/websocket-proxy.cjs +1 -1
  85. package/dist-lib/index.cjs +1843 -408
  86. package/dist-lib/jump-to-source-babel.cjs +1 -1
  87. package/dist-lib/jump-to-source-native.cjs +1 -1
  88. package/dist-lib/kotlin-host.cjs +1 -1
  89. package/dist-lib/menu.cjs +3 -2
  90. package/dist-lib/menu.mjs +3 -2
  91. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  92. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  93. package/dist-lib/metro-production-bundle.cjs +5 -4
  94. package/dist-lib/metro-production-bundle.mjs +5 -4
  95. package/dist-lib/metro.cjs +1182 -418
  96. package/dist-lib/native-app-identity-project.cjs +782 -23
  97. package/dist-lib/native-app-identity-project.mjs +781 -23
  98. package/dist-lib/native-app-identity.cjs +276 -9
  99. package/dist-lib/native-app-identity.mjs +273 -8
  100. package/dist-lib/native-firebase-metadata.cjs +5 -4
  101. package/dist-lib/native-firebase-metadata.mjs +5 -4
  102. package/dist-lib/plist.cjs +1 -1
  103. package/dist-lib/plist.mjs +1 -1
  104. package/dist-lib/profiles.cjs +1 -1
  105. package/dist-lib/public-brand.cjs +3 -2
  106. package/dist-lib/react-native-host-modules.cjs +8 -1
  107. package/dist-lib/react-native-host-modules.mjs +8 -1
  108. package/dist-lib/scripts/dev-server-scanner.cjs +106 -28
  109. package/dist-lib/sdk.cjs +115 -34
  110. package/dist-lib/sdk.mjs +115 -33
  111. package/dist-lib/skills.cjs +15274 -9904
  112. package/dist-lib/swift-compile.cjs +1165 -173
  113. package/dist-lib/swift-compile.mjs +1212 -203
  114. package/dist-lib/swift-package.cjs +12 -3
  115. package/dist-lib/swift-package.mjs +12 -3
  116. package/dist-lib/swift-submit.cjs +7 -1
  117. package/dist-lib/swift-submit.mjs +5 -1
  118. package/dist-lib/swift-toolchain.cjs +133 -18
  119. package/dist-lib/swift.cjs +1 -1
  120. package/dist-lib/vite-swift.cjs +2648 -862
  121. package/dist-lib/vite.cjs +70 -14
  122. package/dist-lib/worklet-executable.cjs +25 -2
  123. package/dist-lib/worklet-executable.mjs +25 -2
  124. package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
  125. package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
  126. package/package.json +2 -2
  127. package/scripts/dev-server-scanner.ts +35 -14
  128. package/skills/peach-perf/SKILL.md +11 -5
  129. package/skills/peach-setup/SKILL.md +16 -19
  130. package/src/admitted-mac-build.ts +34 -8
  131. package/src/android-app-identity.ts +146 -0
  132. package/src/android-autolinking.ts +75 -0
  133. package/src/android-string-value.ts +36 -0
  134. package/src/android-strings.ts +5 -36
  135. package/src/androidAutolinkingContract.ts +40 -0
  136. package/src/bridge-constants.ts +11 -0
  137. package/src/bridge-contract.ts +22 -34
  138. package/src/bundleGlobalFooter.ts +26 -7
  139. package/src/codex-client.ts +3 -2
  140. package/src/config.ts +15 -0
  141. package/src/desktop-host-delivery.ts +5 -1
  142. package/src/dev-bundle-resolution.ts +60 -26
  143. package/src/expo-kotlin-host.ts +6 -36
  144. package/src/expo-manifest-body.ts +13 -0
  145. package/src/expo-swift-bundle.ts +17 -12
  146. package/src/expo-swift-host.ts +640 -192
  147. package/src/expo-swift-ready.ts +78 -0
  148. package/src/expoSwiftContract.ts +19 -0
  149. package/src/home-paths.ts +48 -2
  150. package/src/host/bridge-host.ts +12 -57
  151. package/src/host/staged-host-files.ts +29 -2
  152. package/src/installed-package-versions.ts +54 -14
  153. package/src/launchMetadataContract.ts +24 -0
  154. package/src/metro-plugin.ts +37 -42
  155. package/src/native-app-identity-project.ts +142 -5
  156. package/src/native-app-identity.ts +176 -11
  157. package/src/native-firebase-config.ts +209 -13
  158. package/src/nitro-compile.ts +2 -5
  159. package/src/nitro-source.ts +147 -63
  160. package/src/process-group.ts +114 -61
  161. package/src/process-output.ts +0 -9
  162. package/src/public-brand.ts +1 -0
  163. package/src/react-native-bridge-modules.ts +416 -0
  164. package/src/react-native-host-modules.ts +7 -0
  165. package/src/sim-client.ts +6 -4
  166. package/src/swift-binary-modules.ts +2 -2
  167. package/src/swift-compile.ts +1007 -108
  168. package/src/swift-compiler-digest.ts +14 -0
  169. package/src/swift-distribution.lock.json +40 -12
  170. package/src/swift-distribution.ts +61 -9
  171. package/src/swift-host.ts +149 -56
  172. package/src/swift-icu.ts +14 -6
  173. package/src/swift-objc-sdk.ts +133 -10
  174. package/src/swift-objc-standin.ts +38 -3
  175. package/src/swift-package-manifest.ts +21 -6
  176. package/src/swift-package.ts +15 -1
  177. package/src/swift-packages.ts +119 -60
  178. package/src/swift-standin-emit.ts +57 -9
  179. package/src/swift-standins.ts +185 -25
  180. package/src/swift-submit.ts +10 -0
  181. package/src/swift-toolchain.ts +149 -23
  182. package/src/vite-plugin.ts +20 -20
  183. package/src/worklet-executable.ts +30 -3
  184. package/src/worklets-babel.ts +35 -14
  185. package/src/xcode-project.ts +10 -0
package/dist-lib/sdk.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
4
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -6,14 +6,20 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
6
6
  // src/bridge-contract.ts
7
7
  var SEMANTIC_READY_CONTENT_NODE_FLOOR = 100;
8
8
  var HEADLESS_TENANT_FETCH_HUNG_MARKER = "headless tenant fetch hung";
9
- function readyProbeHasContent(probe) {
10
- return probe.paintedAppContent === true || (probe.browserSurfaceCount ?? 0) > 0 || probe.liveFrameActive || probe.targets > 0 || probe.nodes >= SEMANTIC_READY_CONTENT_NODE_FLOOR;
9
+ function readyProbeContentBasis(probe) {
10
+ if (probe.paintedAppContent === true) return "paintedAppContent";
11
+ if ((probe.browserSurfaceCount ?? 0) > 0) return "browserSurface";
12
+ if (probe.liveFrameActive) return "liveFrame";
13
+ if (probe.targets > 0) return "targets";
14
+ if (probe.flag === true && probe.nodes >= SEMANTIC_READY_CONTENT_NODE_FLOOR)
15
+ return "nodeFloor";
16
+ return "";
11
17
  }
12
- function readyProbeHasTargetContent(probe) {
13
- return probe.paintedAppContent === true || (probe.browserSurfaceCount ?? 0) > 0 || probe.liveFrameActive || probe.targets > 0;
18
+ function readyProbeHasContent(probe) {
19
+ return readyProbeContentBasis({ ...probe, flag: true }) !== "";
14
20
  }
15
21
  function readyProbeIsReady(probe) {
16
- return (probe.flag === true ? readyProbeHasContent(probe) : readyProbeHasTargetContent(probe)) && probe.externalReady !== false && !probe.externalError && !probe.loadingText;
22
+ return readyProbeContentBasis(probe) !== "" && probe.externalReady !== false && !probe.externalError && !probe.loadingText;
17
23
  }
18
24
 
19
25
  // src/bridge-contract-input.ts
@@ -863,7 +869,10 @@ function waitReadyStatus(result, ready) {
863
869
  externalError: probe?.externalError ?? "",
864
870
  suppressedEntryError: probe?.suppressedEntryError ?? "",
865
871
  errors: probe?.errors ?? 0,
866
- bridgeError: result.error ?? ""
872
+ bridgeError: result.error ?? "",
873
+ paintedAppContent: probe?.paintedAppContent ?? false,
874
+ treeQuietMs: probe?.treeQuietMs ?? 0,
875
+ readyBasis: probe ? readyProbeContentBasis(probe) : ""
867
876
  };
868
877
  }
869
878
  async function inspectWaitSelector(bridge, testId, timeoutMs = 5e3, opts = {}) {
@@ -1479,6 +1488,28 @@ async function resolveTargetCoords(bridge, target) {
1479
1488
  };
1480
1489
  }
1481
1490
 
1491
+ // cli/maestro-accessibility.ts
1492
+ function accessibilityBounds(node) {
1493
+ const left = Math.trunc(node.frame.x);
1494
+ const top = Math.trunc(node.frame.y);
1495
+ return {
1496
+ x: left,
1497
+ y: top,
1498
+ width: Math.trunc(node.frame.x + node.frame.width) - left,
1499
+ height: Math.trunc(node.frame.y + node.frame.height) - top
1500
+ };
1501
+ }
1502
+ function sameNativeAccessibilityShape(a, b, bounds) {
1503
+ if ((a.testID ?? "") !== (b.testID ?? "") || (a.label ?? "") !== (b.label ?? "") || (a.placeholder ?? "") !== (b.placeholder ?? "") || (a.value ?? "") !== (b.value ?? "") || a.enabled !== b.enabled || a.selected !== b.selected || a.checked !== b.checked || a.clickable !== b.clickable || (a.hasFocus ?? false) !== (b.hasFocus ?? false) || JSON.stringify(bounds(a)) !== JSON.stringify(bounds(b)) || a.children.length !== b.children.length) {
1504
+ return false;
1505
+ }
1506
+ for (const [index, child] of a.children.entries()) {
1507
+ const other = b.children[index];
1508
+ if (!other || !sameNativeAccessibilityShape(child, other, bounds)) return false;
1509
+ }
1510
+ return true;
1511
+ }
1512
+
1482
1513
  // cli/commands/inspect/settling.ts
1483
1514
  var RuntimeReadinessUnsupportedError = class extends Error {
1484
1515
  constructor(runtimeVersion, missingFields) {
@@ -1511,6 +1542,7 @@ async function waitForSootsimIdle({
1511
1542
  stablePolls = 3,
1512
1543
  strict = false,
1513
1544
  paintStill = false,
1545
+ maestro = false,
1514
1546
  motionOnly = false,
1515
1547
  transitionStartWindowMs = 80,
1516
1548
  transitionTimeoutMs = 4e3
@@ -1542,12 +1574,15 @@ async function waitForSootsimIdle({
1542
1574
  const pollMs = ${Math.max(1, Math.round(pollMs))}
1543
1575
  const strict = ${strict ? "true" : "false"}
1544
1576
  const paintStill = ${paintStill ? "true" : "false"}
1577
+ const maestro = ${maestro ? "true" : "false"}
1578
+ const accessibilityBounds = ${accessibilityBounds.toString()}
1579
+ const sameNativeAccessibilityShape = ${sameNativeAccessibilityShape.toString()}
1545
1580
  const motionOnly = ${motionOnly ? "true" : "false"}
1546
1581
  // full mounted-geometry observation needs at least two hierarchy reads
1547
1582
  // plus the stillness window, which a sub-second budget cannot afford:
1548
1583
  // tight budgets (auto-wait) settle on worker stillness plus root
1549
1584
  // membership, generous ones confirm the geometry too.
1550
- const fullGeometry = ${!motionOnly && maxMs >= 1e3 ? "true" : "false"}
1585
+ const fullGeometry = ${!maestro && !motionOnly && maxMs >= 1e3 ? "true" : "false"}
1551
1586
  const requiredStillMs = paintStill ? 0 : ${Math.max(1, Math.round(stablePolls)) * Math.max(1, Math.round(pollMs))}
1552
1587
  const minimumStablePolls = paintStill ? 1 : ${Math.min(2, Math.max(1, Math.round(stablePolls)))}
1553
1588
  const composite = paintStill ? window.SootSim?.bridges?.liveComposite : null
@@ -1669,6 +1704,9 @@ async function waitForSootsimIdle({
1669
1704
  // strict also waits out shell motion: a system alert, sheet or menu
1670
1705
  // animates in the shell worker while the app's own stats read still,
1671
1706
  // so its presentation is complete only once the shell's list empties.
1707
+ const accessibilityRead = maestro
1708
+ ? answer(() => window.__sootsimTest.captureAccessibilityTree()).catch(() => null)
1709
+ : null
1672
1710
  const motionRead = strict
1673
1711
  ? answer(() => window.__sootsimTest?.listAnimations?.()).catch(() => null)
1674
1712
  : null
@@ -1678,9 +1716,12 @@ async function waitForSootsimIdle({
1678
1716
  // the compositor rejects a stats request it is too busy to answer.
1679
1717
  const compositorRead = answer(() => window.__sootsimCompositor.getStats()).catch(() => BUSY)
1680
1718
  const countsRead = motionOnly ? null : answer(() => window.__sootsimTest?.getRequestCounts?.()).catch(() => null)
1681
- // mounted-root membership: the live node count moves while the tree
1682
- // mounts, unmounts or recycles, in either render mode.
1683
- const nodeCountRead = answer(() => window.__sootsimTest?.getNodeCount?.()).catch(() => null)
1719
+ // mounted-root membership: the count of nodes that can reach the
1720
+ // window moves while the screen mounts, unmounts or recycles, in
1721
+ // either render mode. a screen a pushed one covers or an inactive
1722
+ // tab stays mounted out of the window, so what it mounts is not
1723
+ // screen motion.
1724
+ const nodeCountRead = answer(() => window.__sootsimTest?.getWindowNodeCount?.()).catch(() => null)
1684
1725
  // the mounted hierarchy in render-worker mode comes from the test
1685
1726
  // bridge: window.__sootsimRoot is an empty placeholder there, so a
1686
1727
  // walk over it hashes every screen to the same empty string while
@@ -1710,7 +1751,7 @@ async function waitForSootsimIdle({
1710
1751
  const motion = await motionRead
1711
1752
  if (motion === BUSY) workerBusy = true
1712
1753
  else if (Array.isArray(motion)) {
1713
- const blocking = motion.filter((anim) => anim.realm === 'shell' && anim.priority !== 'low')
1754
+ const blocking = motion.filter((anim) => anim.realm === 'shell' && anim.priority !== 'low' && anim.visible !== false)
1714
1755
  shellAnimating = blocking.length > 0
1715
1756
  // name what holds the settle so a timeout points at its owner.
1716
1757
  shellBlockers = blocking
@@ -1742,6 +1783,8 @@ async function waitForSootsimIdle({
1742
1783
  stats.hasActiveAnims === true ||
1743
1784
  stats.hasActiveBlockingNativeAnimations === true ||
1744
1785
  (!paintStill && stats.hasPendingBlockingAnimationFrames === true)
1786
+ // the runtime reports only layout work that can reach the window:
1787
+ // a covered screen or inactive tab committing does not hold it.
1745
1788
  layoutDirty =
1746
1789
  stats.layoutDirty === true && stats.renderRequested === true
1747
1790
  // a freshly-pushed screen showing a skeleton is layout-stable
@@ -1774,6 +1817,11 @@ async function waitForSootsimIdle({
1774
1817
  const total = await nodeCountRead
1775
1818
  if (total === BUSY) workerBusy = true
1776
1819
  nodeCount = typeof total === 'number' ? total : null
1820
+ const accessibility = accessibilityRead ? await accessibilityRead : null
1821
+ const nativeHierarchy = Array.isArray(accessibility) && accessibility.length > 0
1822
+ ? accessibility
1823
+ : null
1824
+ if (accessibility === BUSY) workerBusy = true
1777
1825
  let hierarchy = hierarchyRead ? await hierarchyRead : null
1778
1826
  if (hierarchy === BUSY) {
1779
1827
  workerBusy = true
@@ -1833,9 +1881,15 @@ async function waitForSootsimIdle({
1833
1881
  webViewStarting = true
1834
1882
  webViewBlocker = 'preview progress element displayed'
1835
1883
  } else {
1836
- const unreadyWebView = document.querySelector(
1884
+ // a loading document with no area (an app's hidden worker or
1885
+ // executor frame) paints nothing, and a screenshot on device
1886
+ // does not wait for it.
1887
+ const unreadyWebView = [...document.querySelectorAll(
1837
1888
  '[data-sootsim-webview-loading="true"], [data-sootsim-webview-mode="placeholder"]'
1838
- )
1889
+ )].find((view) => {
1890
+ const frame = view.querySelector('iframe')
1891
+ return !frame || frame.clientWidth > 0 || frame.clientHeight > 0
1892
+ })
1839
1893
  if (unreadyWebView) {
1840
1894
  webViewStarting = true
1841
1895
  webViewBlocker = 'webView document still loading'
@@ -1854,7 +1908,7 @@ async function waitForSootsimIdle({
1854
1908
  pixelFrameToken = token
1855
1909
  }
1856
1910
  }
1857
- return { layout: nodes.join(';'), screenPixels, animating, shellAnimating, shellBlockers, layoutDirty, pendingFetches, workerBusy, webViewStarting, webViewBlocker, renderStatsAvailable, requestTotal, requestInFlight, appCommits, nodeCount }
1911
+ return { layout: nodes.join(';'), nativeHierarchy, screenPixels, animating, shellAnimating, shellBlockers, layoutDirty, pendingFetches, workerBusy, webViewStarting, webViewBlocker, renderStatsAvailable, requestTotal, requestInFlight, appCommits, nodeCount }
1858
1912
  }
1859
1913
 
1860
1914
  // how long generic background network is allowed to keep us waiting AFTER
@@ -1868,6 +1922,7 @@ async function waitForSootsimIdle({
1868
1922
  // so the generic request counters are a courtesy, not a gate: honor them
1869
1923
  // only within this grace once the screen is visually still.
1870
1924
  const networkQuietGraceMs = 1200
1925
+ let lastNativeHierarchy = null
1871
1926
  let lastLayout = ''
1872
1927
  let lastScreenPixels = null
1873
1928
  let screenChangedPolls = 0
@@ -1895,6 +1950,9 @@ async function waitForSootsimIdle({
1895
1950
  }
1896
1951
  return true
1897
1952
  }
1953
+ const hierarchyUnchanged = (current, previous) =>
1954
+ current !== null && previous !== null && current.length === previous.length &&
1955
+ current.every((node, index) => sameNativeAccessibilityShape(node, previous[index], accessibilityBounds))
1898
1956
  const IDLE_HELD = { idleHeld: true }
1899
1957
  while (true) {
1900
1958
  if (clockNow() >= deadline) {
@@ -1940,7 +1998,7 @@ async function waitForSootsimIdle({
1940
1998
  // the loop head drains a transition still in flight before it ends.
1941
1999
  if (snapshot.workerBusy && clockNow() >= deadline && report) continue
1942
2000
  polls++
1943
- report = { snapshot, layout: lastLayout, screenPixels: lastScreenPixels, requestTotal: lastRequestTotal, appCommits: lastAppCommits, nodeCount: lastNodeCount }
2001
+ report = { snapshot, nativeHierarchy: lastNativeHierarchy, layout: lastLayout, screenPixels: lastScreenPixels, requestTotal: lastRequestTotal, appCommits: lastAppCommits, nodeCount: lastNodeCount }
1944
2002
  if (snapshot.layoutDirty) dirtyPolls++
1945
2003
  if (snapshot.animating || snapshot.shellAnimating) animatingPolls++
1946
2004
  if (snapshot.webViewStarting) webViewStartingPolls++
@@ -1961,7 +2019,15 @@ async function waitForSootsimIdle({
1961
2019
  // strictOk and adds shell motion plus the blocking-stats floor. a
1962
2020
  // null count or commit reading means the runtime does not report
1963
2021
  // it, which reads as stable the way a missing stats block does.
1964
- const visuallyStill =
2022
+ // native screen checks observe output, not animation ownership. after
2023
+ // its 3 s pixel phase, iOS accepts two equal native TreeNodes even
2024
+ // while paint-only playback continues. overlap hierarchy reads so
2025
+ // neither the gesture nor screenshot deadline grows.
2026
+ const maestroStill = snapshot.screenPixels !== null && !snapshot.workerBusy && !snapshot.webViewStarting && (
2027
+ !screenChanged || (clockNow() - start >= 3000 &&
2028
+ hierarchyUnchanged(snapshot.nativeHierarchy, lastNativeHierarchy))
2029
+ )
2030
+ const visuallyStill = maestro ? maestroStill :
1965
2031
  strictOk &&
1966
2032
  !snapshot.animating &&
1967
2033
  !snapshot.layoutDirty &&
@@ -1989,7 +2055,7 @@ async function waitForSootsimIdle({
1989
2055
  // grace (continuous background traffic must not block forever).
1990
2056
  if (
1991
2057
  stillProven() &&
1992
- (motionOnly || networkQuiet || clockNow() - visuallyStillSince >= networkQuietGraceMs)
2058
+ (maestro || motionOnly || networkQuiet || clockNow() - visuallyStillSince >= networkQuietGraceMs)
1993
2059
  ) {
1994
2060
  if (transitionWait) {
1995
2061
  await transitionWait
@@ -2001,6 +2067,7 @@ async function waitForSootsimIdle({
2001
2067
  stillPolls = []
2002
2068
  visuallyStillSince = 0
2003
2069
  }
2070
+ lastNativeHierarchy = snapshot.nativeHierarchy
2004
2071
  lastLayout = snapshot.layout
2005
2072
  lastScreenPixels = snapshot.screenPixels
2006
2073
  lastRequestTotal = snapshot.requestTotal
@@ -2023,20 +2090,21 @@ async function waitForSootsimIdle({
2023
2090
  }
2024
2091
  const last = report.snapshot
2025
2092
  const blockers = []
2026
- if (strict && !last.renderStatsAvailable) blockers.push('render stats unavailable')
2027
- if (last.animating) blockers.push('animations still running')
2028
- if (last.shellAnimating) blockers.push('shell animations still running (' + last.shellBlockers + ')')
2029
- if (last.layoutDirty) blockers.push('layout still dirty')
2030
- if (last.appCommits !== null && report.appCommits !== null && last.appCommits !== report.appCommits) blockers.push('layout commits still landing')
2031
- if (last.nodeCount !== null && report.nodeCount !== null && last.nodeCount !== report.nodeCount) blockers.push('node count still changing')
2032
- if (!motionOnly && last.pendingFetches > 0) blockers.push(last.pendingFetches + ' image fetches pending')
2093
+ if (!maestro && strict && !last.renderStatsAvailable) blockers.push('render stats unavailable')
2094
+ if (!maestro && last.animating) blockers.push('animations still running')
2095
+ if (!maestro && last.shellAnimating) blockers.push('shell animations still running (' + last.shellBlockers + ')')
2096
+ if (!maestro && last.layoutDirty) blockers.push('layout still dirty')
2097
+ if (!maestro && last.appCommits !== null && report.appCommits !== null && last.appCommits !== report.appCommits) blockers.push('layout commits still landing')
2098
+ if (!maestro && last.nodeCount !== null && report.nodeCount !== null && last.nodeCount !== report.nodeCount) blockers.push('node count still changing')
2099
+ if (!maestro && !motionOnly && last.pendingFetches > 0) blockers.push(last.pendingFetches + ' image fetches pending')
2033
2100
  if (last.workerBusy) blockers.push('a render worker too busy to report')
2034
2101
  if (last.webViewStarting) blockers.push(last.webViewBlocker || 'webView still starting')
2035
- if (last.layout !== report.layout) blockers.push('layout still changing')
2102
+ if (!maestro && last.layout !== report.layout) blockers.push('layout still changing')
2103
+ if (maestro && !hierarchyUnchanged(last.nativeHierarchy, report.nativeHierarchy)) blockers.push(last.nativeHierarchy === null ? 'native accessibility hierarchy unavailable' : 'native accessibility hierarchy still changing')
2036
2104
  if (paintStill && !last.screenPixels) blockers.push('screen pixels unavailable')
2037
2105
  else if (paintStill && !screenUnchanged(last.screenPixels, report.screenPixels)) blockers.push('screen pixels still changing')
2038
- if (!motionOnly && last.requestInFlight > 0) blockers.push(last.requestInFlight + ' requests in flight')
2039
- else if (!motionOnly && last.requestTotal !== report.requestTotal) blockers.push('new requests issued')
2106
+ if (!maestro && !motionOnly && last.requestInFlight > 0) blockers.push(last.requestInFlight + ' requests in flight')
2107
+ else if (!maestro && !motionOnly && last.requestTotal !== report.requestTotal) blockers.push('new requests issued')
2040
2108
  if (polls < 2) blockers.push('sim too busy to poll twice')
2041
2109
  // the last read is still: motion ended too near the deadline for the
2042
2110
  // stillness window to hold before it.
@@ -2078,8 +2146,10 @@ async function waitForSootsimIdle({
2078
2146
  async function waitReadyAndSettle(bridge, timeoutMs, options = {}) {
2079
2147
  const start = Date.now();
2080
2148
  const remaining = () => Math.max(0, start + timeoutMs - Date.now());
2149
+ let accepted = null;
2081
2150
  const result = (status, idle2) => ({
2082
2151
  status,
2152
+ accepted,
2083
2153
  idle: idle2,
2084
2154
  elapsedMs: Date.now() - start
2085
2155
  });
@@ -2088,6 +2158,7 @@ async function waitReadyAndSettle(bridge, timeoutMs, options = {}) {
2088
2158
  idle = null;
2089
2159
  const ready = await inspectWaitReady(bridge, remaining(), options);
2090
2160
  if (!ready.ready) return result(ready, idle);
2161
+ accepted = ready;
2091
2162
  idle = await waitForSootsimIdle({
2092
2163
  bridge,
2093
2164
  simId: options.simId,
@@ -2511,6 +2582,20 @@ async function openAppUrl(bridge, target) {
2511
2582
  return normalized;
2512
2583
  }
2513
2584
 
2585
+ // src/launchMetadataContract.ts
2586
+ var LAUNCH_METADATA_QUERY_PARAM = "launchMetadata";
2587
+ var LAUNCH_METADATA_KEYS = [
2588
+ "appFonts",
2589
+ "appSplash",
2590
+ "appCatalog",
2591
+ "nativeAppIdentity",
2592
+ "nativeFirebase",
2593
+ "appManifest",
2594
+ "packageVersions",
2595
+ "dotEnv",
2596
+ "env"
2597
+ ];
2598
+
2514
2599
  // src/sleep.ts
2515
2600
  function sleep(ms) {
2516
2601
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -2579,10 +2664,8 @@ var TARGET_SEARCH_PARAMS = [
2579
2664
  "open",
2580
2665
  "port",
2581
2666
  "inspectOpen",
2582
- "appFonts",
2583
- "appManifest",
2584
- "appSplash",
2585
- "appCatalog"
2667
+ ...LAUNCH_METADATA_KEYS,
2668
+ LAUNCH_METADATA_QUERY_PARAM
2586
2669
  ];
2587
2670
  function buildSimOpenUrl(currentUrl, target) {
2588
2671
  const url = new URL(currentUrl);
@@ -3037,7 +3120,6 @@ export {
3037
3120
  layoutGetNativeEval,
3038
3121
  rankInteractive,
3039
3122
  readyProbeHasContent,
3040
- readyProbeHasTargetContent,
3041
3123
  readyProbeIsReady,
3042
3124
  resolveFindMode,
3043
3125
  resolveMaxMsFlag,