peachsim 0.1.1406 → 0.1.1463

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 (149) hide show
  1. package/LICENSE +8 -4
  2. package/README.md +22 -2
  3. package/cli/app-fonts.ts +50 -44
  4. package/cli/bin.ts +5 -4
  5. package/cli/bridge-diagnostics.ts +5 -0
  6. package/cli/bridge-flow-runner.ts +21 -13
  7. package/cli/cloud-client.ts +4 -3
  8. package/cli/commands/control.ts +155 -123
  9. package/cli/commands/daemon-mac-app.ts +3 -3
  10. package/cli/commands/daemon.ts +2 -2
  11. package/cli/commands/do-chain.ts +9 -2
  12. package/cli/commands/electron.ts +4 -1
  13. package/cli/commands/inspect/core.ts +1 -1
  14. package/cli/commands/inspect/settle.ts +19 -7
  15. package/cli/commands/inspect/settling.ts +92 -56
  16. package/cli/commands/inspect/wait-idle.ts +21 -9
  17. package/cli/commands/inspect/wait-ready.ts +22 -8
  18. package/cli/commands/inspect.ts +101 -134
  19. package/cli/commands/setup.ts +26 -16
  20. package/cli/commands/upgrade.ts +53 -9
  21. package/cli/desktop-companion.ts +1 -1
  22. package/cli/drivers/playwright-sim-host.ts +22 -13
  23. package/cli/drivers/playwright.ts +3 -3
  24. package/cli/main.ts +14 -5
  25. package/cli/outbound-endpoints.ts +1 -9
  26. package/detox/index.ts +6 -1
  27. package/dist-lib/agent-daemon-client.cjs +1 -1
  28. package/dist-lib/agent-events.cjs +1 -1
  29. package/dist-lib/attached-projects.cjs +1 -1
  30. package/dist-lib/auth/shared-session.cjs +1 -1
  31. package/dist-lib/backend-origin.cjs +1 -1
  32. package/dist-lib/beta.cjs +1 -1
  33. package/dist-lib/beta.mjs +1 -1
  34. package/dist-lib/bridge-constants.cjs +1 -1
  35. package/dist-lib/bridge-contract-input.cjs +1 -1
  36. package/dist-lib/bridge-contract-input.mjs +1 -1
  37. package/dist-lib/bridge-contract.cjs +3 -3
  38. package/dist-lib/bridge-contract.mjs +3 -3
  39. package/dist-lib/bundle-channel.cjs +322 -274
  40. package/dist-lib/capture-contract.cjs +1 -1
  41. package/dist-lib/capture-contract.mjs +1 -1
  42. package/dist-lib/cloud-contract.cjs +1 -1
  43. package/dist-lib/cloud-contract.mjs +1 -1
  44. package/dist-lib/cloud-test-args.cjs +1 -1
  45. package/dist-lib/cloud-test-args.mjs +1 -1
  46. package/dist-lib/cloud.cjs +1 -1
  47. package/dist-lib/cloud.mjs +1 -1
  48. package/dist-lib/config.cjs +1 -1
  49. package/dist-lib/config.mjs +1 -1
  50. package/dist-lib/detox/index.cjs +322 -45
  51. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  52. package/dist-lib/expo-swift-bundle.cjs +69 -33
  53. package/dist-lib/expo-swift-metadata.cjs +1 -1
  54. package/dist-lib/expo-swift-metadata.mjs +1 -1
  55. package/dist-lib/home-paths.cjs +1 -1
  56. package/dist-lib/host/bridge-host.cjs +3650 -3528
  57. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  58. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  59. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  60. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  61. package/dist-lib/host/websocket-proxy.cjs +1 -1
  62. package/dist-lib/index.cjs +327 -123
  63. package/dist-lib/jump-to-source-babel.cjs +1 -1
  64. package/dist-lib/jump-to-source-native.cjs +1 -1
  65. package/dist-lib/kotlin-host.cjs +10 -5
  66. package/dist-lib/menu.cjs +1 -1
  67. package/dist-lib/menu.mjs +1 -1
  68. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  69. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  70. package/dist-lib/metro-production-bundle.cjs +1 -1
  71. package/dist-lib/metro-production-bundle.mjs +1 -1
  72. package/dist-lib/metro.cjs +49 -47
  73. package/dist-lib/native-app-identity-project.cjs +1 -1
  74. package/dist-lib/native-app-identity-project.mjs +1 -1
  75. package/dist-lib/native-app-identity.cjs +1 -1
  76. package/dist-lib/native-app-identity.mjs +1 -1
  77. package/dist-lib/native-firebase-metadata.cjs +1 -1
  78. package/dist-lib/native-firebase-metadata.mjs +1 -1
  79. package/dist-lib/plist.cjs +1 -1
  80. package/dist-lib/plist.mjs +1 -1
  81. package/dist-lib/profiles.cjs +1 -1
  82. package/dist-lib/public-brand.cjs +1 -1
  83. package/dist-lib/react-native-host-modules.cjs +1 -1
  84. package/dist-lib/react-native-host-modules.mjs +1 -1
  85. package/dist-lib/scripts/dev-server-scanner.cjs +23 -17
  86. package/dist-lib/sdk.cjs +99 -61
  87. package/dist-lib/sdk.mjs +97 -61
  88. package/dist-lib/skills.cjs +2959 -2780
  89. package/dist-lib/swift-compile.cjs +26 -12
  90. package/dist-lib/swift-compile.mjs +26 -12
  91. package/dist-lib/swift-package.cjs +1 -1
  92. package/dist-lib/swift-package.mjs +1 -1
  93. package/dist-lib/swift-submit.cjs +2 -2
  94. package/dist-lib/swift-submit.mjs +2 -2
  95. package/dist-lib/swift-toolchain.cjs +1 -1
  96. package/dist-lib/swift.cjs +1 -1
  97. package/dist-lib/vite-swift.cjs +66 -31
  98. package/dist-lib/vite.cjs +1 -1
  99. package/dist-lib/worklet-executable.cjs +1 -1
  100. package/dist-lib/worklet-executable.mjs +1 -1
  101. package/kotlin/AppSources/android/view/Window.kt +2 -0
  102. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +1 -1
  103. package/kotlin/ComposeSources/PeachComposeHost.kt +35 -9
  104. package/kotlin/ExpoComposeSources/dev/peach/expo/PeachExpoCompose.kt +2 -2
  105. package/kotlin/ExpoSources/dev/peach/expo/PeachExpoRuntime.kt +9 -6
  106. package/kotlin/ExpoSources/dev/peach/expo/PeachExpoTypes.kt +4 -4
  107. package/kotlin/ExpoSources/expo/modules/kotlin/modules/Module.kt +2 -2
  108. package/kotlin/ExpoSources/expo/modules/kotlin/viewevent/ViewEvent.kt +22 -8
  109. package/kotlin/ExpoSources/expo/modules/kotlin/views/ExpoView.kt +0 -3
  110. package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewDefinitionBuilder.kt +10 -9
  111. package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewProps.kt +6 -5
  112. package/kotlin/fixtures/import-drawing/Drawing.d.kt.ts +7 -0
  113. package/kotlin/fixtures/import-drawing/Drawing.kt +101 -0
  114. package/kotlin/fixtures/import-drawing/ImportDrawing.native.tsx +18 -0
  115. package/kotlin/fixtures/import-drawing/README.md +34 -0
  116. package/kotlin/fixtures/import-drawing/app/_layout.tsx +4 -0
  117. package/kotlin/fixtures/import-drawing/app/index+spa.tsx +1 -0
  118. package/kotlin/fixtures/import-drawing/package.json +23 -0
  119. package/kotlin/fixtures/import-drawing/tsconfig.json +20 -0
  120. package/kotlin/fixtures/import-drawing/vite.config.ts +20 -0
  121. package/kotlin/fixtures/import-scroll/ImportNested.native.tsx +10 -0
  122. package/kotlin/fixtures/import-scroll/Nested.d.kt.ts +4 -0
  123. package/kotlin/fixtures/import-scroll/README.md +15 -0
  124. package/kotlin/fixtures/import-scroll/app/nested+spa.tsx +1 -0
  125. package/kotlin/fixtures/import-values/ImportValues.native.tsx +26 -0
  126. package/kotlin/fixtures/import-values/README.md +6 -1
  127. package/kotlin/fixtures/import-values/Values.d.kt.ts +4 -0
  128. package/kotlin/fixtures/import-values/Values.kt +18 -0
  129. package/kotlin/tools/expo-types.main.kts +17 -5
  130. package/package.json +2 -1
  131. package/scripts/dev-server-scanner.ts +33 -23
  132. package/src/assetCatalogContract.ts +7 -0
  133. package/src/bridge-contract.ts +21 -3
  134. package/src/connect.ts +18 -5
  135. package/src/desktop-host-delivery.ts +33 -3
  136. package/src/expo-swift-host.ts +6 -3
  137. package/src/host/bridge-host.ts +2 -0
  138. package/src/host/fetch-proxy-transport.ts +8 -0
  139. package/src/kotlin-host.ts +11 -5
  140. package/src/native-firebase-config.ts +95 -54
  141. package/src/process-output.ts +16 -0
  142. package/src/sdk.ts +6 -1
  143. package/src/sim-client.ts +6 -1
  144. package/src/swift-compile.ts +36 -15
  145. package/src/swift-distribution.lock.json +8 -8
  146. package/src/swift-standin-emit.ts +10 -1
  147. package/src/swift-standins.ts +21 -7
  148. package/src/swift-submit.ts +6 -1
  149. /package/kotlin/fixtures/{kotlin-app-variants → import-scroll}/Nested.kt +0 -0
package/dist-lib/sdk.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1406 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1463 | (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);
@@ -7,10 +7,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
7
7
  var SEMANTIC_READY_CONTENT_NODE_FLOOR = 100;
8
8
  var HEADLESS_TENANT_FETCH_HUNG_MARKER = "headless tenant fetch hung";
9
9
  function readyProbeHasContent(probe) {
10
- return (probe.browserSurfaceCount ?? 0) > 0 || probe.liveFrameActive || probe.targets > 0 || probe.nodes >= SEMANTIC_READY_CONTENT_NODE_FLOOR;
10
+ return probe.paintedAppContent === true || (probe.browserSurfaceCount ?? 0) > 0 || probe.liveFrameActive || probe.targets > 0 || probe.nodes >= SEMANTIC_READY_CONTENT_NODE_FLOOR;
11
11
  }
12
12
  function readyProbeHasTargetContent(probe) {
13
- return (probe.browserSurfaceCount ?? 0) > 0 || probe.liveFrameActive || probe.targets > 0;
13
+ return probe.paintedAppContent === true || (probe.browserSurfaceCount ?? 0) > 0 || probe.liveFrameActive || probe.targets > 0;
14
14
  }
15
15
  function readyProbeIsReady(probe) {
16
16
  return (probe.flag === true ? readyProbeHasContent(probe) : readyProbeHasTargetContent(probe)) && probe.externalReady !== false && !probe.externalError && !probe.loadingText;
@@ -1336,7 +1336,7 @@ function formatTimelinePayload(kind, d) {
1336
1336
  case "notification":
1337
1337
  return `${d.title ?? ""}${d.body ? ` \u2014 ${d.body}` : ""}`;
1338
1338
  case "fetch":
1339
- return `${d.method ?? "GET"} ${d.url ?? ""}${d.status ? ` -> ${d.status}` : ""}`;
1339
+ return `${d.method ?? "GET"} ${d.url ?? ""}${d.status ? ` -> ${d.status}` : ""}${d.error ? ` -> ${d.error}` : ""}`;
1340
1340
  case "console":
1341
1341
  case "console-log":
1342
1342
  return `${d.level ?? "log"}: ${(d.message ?? "").toString().slice(0, 120)}`;
@@ -1431,6 +1431,18 @@ var RuntimeReadinessUnsupportedError = class extends Error {
1431
1431
  this.name = "RuntimeReadinessUnsupportedError";
1432
1432
  }
1433
1433
  };
1434
+ var IdleObservationError = class extends Error {
1435
+ constructor(elapsedMs, failure) {
1436
+ const detail = failure instanceof Error ? failure.message : String(failure);
1437
+ super(`idle observation failed after ${elapsedMs}ms: ${detail}`, {
1438
+ cause: failure
1439
+ });
1440
+ this.elapsedMs = elapsedMs;
1441
+ this.failure = failure;
1442
+ __publicField(this, "code", "idle_observation_failed");
1443
+ this.name = "IdleObservationError";
1444
+ }
1445
+ };
1434
1446
  async function waitForSootsimIdle({
1435
1447
  bridge,
1436
1448
  simId,
@@ -1441,6 +1453,7 @@ async function waitForSootsimIdle({
1441
1453
  paintStill = false
1442
1454
  }) {
1443
1455
  let result;
1456
+ const observationStartedAt = Date.now();
1444
1457
  try {
1445
1458
  result = await bridge.send(
1446
1459
  {
@@ -1471,27 +1484,23 @@ async function waitForSootsimIdle({
1471
1484
  let screenPixels = null
1472
1485
  try {
1473
1486
 
1474
- // route-aware settle: a tap that pushes/pops a screen is async \u2014 the
1475
- // old screen still renders for a moment, then the new one mounts and
1476
- // its content loads. a pure layout-hash check reports "idle" on the
1477
- // outgoing screen or on the incoming skeleton, so a driver re-taps
1478
- // and stacks duplicate navigations. drain any in-flight screen
1479
- // transition FIRST, bounded by the overall budget. when no
1480
- // transition is happening this returns in ~80ms (startWindowMs), so
1481
- // a plain button tap barely pays for it.
1482
- try {
1483
- const wfst = window.__sootsimTest?.waitForScreenTransitions
1484
- if (typeof wfst === 'function') {
1487
+ // discover transitions while observing layout. when motion starts,
1488
+ // observations from before its completion cannot prove the new screen.
1489
+ let transitionWait = (async () => {
1490
+ try {
1491
+ const wfst = window.__sootsimTest?.waitForScreenTransitions
1485
1492
  const remaining = deadline - Date.now()
1486
- if (remaining > 120) {
1487
- await wfst({
1493
+ if (typeof wfst === 'function' && remaining > 120) {
1494
+ const transition = await wfst({
1488
1495
  timeoutMs: Math.min(remaining - 80, 4000),
1489
1496
  settleMs: 64,
1490
1497
  startWindowMs: 80,
1491
1498
  })
1499
+ return transition?.started === true
1492
1500
  }
1493
- }
1494
- } catch {}
1501
+ } catch {}
1502
+ return false
1503
+ })()
1495
1504
 
1496
1505
  // a worker that cannot answer within the remaining budget is busy, not
1497
1506
  // idle: its poll reads as unsettled. bounding each read by the budget
@@ -1516,17 +1525,51 @@ async function waitForSootsimIdle({
1516
1525
  let nodeCount = null
1517
1526
  let renderStatsAvailable = false
1518
1527
  let shellAnimating = false
1528
+ let shellBlockers = ''
1519
1529
  let webViewStarting = false
1520
1530
  let webViewBlocker = ''
1531
+ // the reads are independent, so they run concurrently: a loaded sim
1532
+ // answers each one late, and a poll that waits for them in turn
1533
+ // costs their sum, which fits too few polls in the budget to settle.
1521
1534
  // strict also waits out shell motion: a system alert, sheet or menu
1522
1535
  // animates in the shell worker while the app's own stats read still,
1523
1536
  // so its presentation is complete only once the shell's list empties.
1524
- if (strict) {
1525
- const motion = await answer(() => window.__sootsimTest?.listAnimations?.()).catch(() => null)
1537
+ const motionRead = strict
1538
+ ? answer(() => window.__sootsimTest?.listAnimations?.()).catch(() => null)
1539
+ : null
1540
+ const statsRead = answer(() => window.__sootsimRenderHost?.queryStats?.()).catch(() => null)
1541
+ // image preparation ends in the tenant before the compositor decodes
1542
+ // and realizes its pixels. both owners must finish before settling.
1543
+ // the compositor rejects a stats request it is too busy to answer.
1544
+ const compositorRead = answer(() => window.__sootsimCompositor.getStats()).catch(() => BUSY)
1545
+ const countsRead = Promise.resolve().then(() => window.__sootsimTest?.getRequestCounts?.()).catch(() => null)
1546
+ // mounted-root membership: the live node count moves while the tree
1547
+ // mounts, unmounts or recycles, in either render mode.
1548
+ const nodeCountRead = Promise.resolve().then(() => window.__sootsimTest?.getNodeCount?.()).catch(() => null)
1549
+ // the mounted hierarchy in render-worker mode comes from the test
1550
+ // bridge: window.__sootsimRoot is an empty placeholder there, so a
1551
+ // walk over it hashes every screen to the same empty string while
1552
+ // it moves. listInspectable is the worker-backed read describe
1553
+ // uses; a worker too busy to answer it reads as unsettled. tight
1554
+ // budgets skip this read and settle on the cheaper signals above.
1555
+ const hierarchyRead =
1556
+ fullGeometry && typeof window.__sootsimTest?.listInspectable === 'function'
1557
+ ? answer(() => window.__sootsimTest.listInspectable({})).catch(() => null)
1558
+ : null
1559
+ if (motionRead) {
1560
+ const motion = await motionRead
1526
1561
  if (motion === BUSY) workerBusy = true
1527
- else shellAnimating = Array.isArray(motion) && motion.some((anim) => anim.realm === 'shell' && anim.priority !== 'low')
1562
+ else if (Array.isArray(motion)) {
1563
+ const blocking = motion.filter((anim) => anim.realm === 'shell' && anim.priority !== 'low')
1564
+ shellAnimating = blocking.length > 0
1565
+ // name what holds the settle so a timeout points at its owner.
1566
+ shellBlockers = blocking
1567
+ .slice(0, 4)
1568
+ .map((anim) => anim.source + '/' + anim.kind + '#' + anim.id + ' at ' + anim.current + (anim.loop ? ' loop' : ''))
1569
+ .join(', ')
1570
+ }
1528
1571
  }
1529
- const stats = await answer(() => window.__sootsimRenderHost?.queryStats?.()).catch(() => null)
1572
+ const stats = await statsRead
1530
1573
  if (stats === BUSY) {
1531
1574
  workerBusy = true
1532
1575
  } else if (stats) {
@@ -1563,10 +1606,7 @@ async function waitForSootsimIdle({
1563
1606
  // mounting or remounting, even with no animation flag raised.
1564
1607
  appCommits = typeof stats.appCommitCount === 'number' ? stats.appCommitCount : null
1565
1608
  }
1566
- // image preparation ends in the tenant before the compositor decodes
1567
- // and realizes its pixels. both owners must finish before settling.
1568
- // the compositor rejects a stats request it is too busy to answer.
1569
- const compositor = await answer(() => window.__sootsimCompositor.getStats()).catch(() => BUSY)
1609
+ const compositor = await compositorRead
1570
1610
  if (compositor === BUSY) {
1571
1611
  workerBusy = true
1572
1612
  } else {
@@ -1577,31 +1617,12 @@ async function waitForSootsimIdle({
1577
1617
  webViewBlocker = 'compositor webView capture not settled'
1578
1618
  }
1579
1619
  }
1580
- try {
1581
- const counts = await window.__sootsimTest?.getRequestCounts?.()
1582
- requestTotal = typeof counts?.total === 'number' ? counts.total : 0
1583
- requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
1584
- } catch {}
1585
- // mounted-root membership: the live node count moves while the tree
1586
- // mounts, unmounts or recycles, in either render mode.
1587
- try {
1588
- const total = await window.__sootsimTest?.getNodeCount?.()
1589
- nodeCount = typeof total === 'number' ? total : null
1590
- } catch {}
1591
- // the mounted hierarchy in render-worker mode comes from the test
1592
- // bridge: window.__sootsimRoot is an empty placeholder there, so a
1593
- // walk over it hashes every screen to the same empty string while
1594
- // it moves. listInspectable is the worker-backed read describe
1595
- // uses; a worker too busy to answer it reads as unsettled. tight
1596
- // budgets skip this read and settle on the cheaper signals above.
1597
- let hierarchy = null
1598
- if (fullGeometry && typeof window.__sootsimTest?.listInspectable === 'function') {
1599
- try {
1600
- hierarchy = await answer(() => window.__sootsimTest.listInspectable({}))
1601
- } catch {
1602
- hierarchy = null
1603
- }
1604
- }
1620
+ const counts = await countsRead
1621
+ requestTotal = typeof counts?.total === 'number' ? counts.total : 0
1622
+ requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
1623
+ const total = await nodeCountRead
1624
+ nodeCount = typeof total === 'number' ? total : null
1625
+ let hierarchy = hierarchyRead ? await hierarchyRead : null
1605
1626
  if (hierarchy === BUSY) {
1606
1627
  workerBusy = true
1607
1628
  hierarchy = null
@@ -1677,7 +1698,7 @@ async function waitForSootsimIdle({
1677
1698
  pixelFrameToken = token
1678
1699
  }
1679
1700
  }
1680
- return { layout: nodes.join(';'), screenPixels, animating, shellAnimating, layoutDirty, pendingFetches, workerBusy, webViewStarting, webViewBlocker, renderStatsAvailable, requestTotal, requestInFlight, appCommits, nodeCount }
1701
+ return { layout: nodes.join(';'), screenPixels, animating, shellAnimating, shellBlockers, layoutDirty, pendingFetches, workerBusy, webViewStarting, webViewBlocker, renderStatsAvailable, requestTotal, requestInFlight, appCommits, nodeCount }
1681
1702
  }
1682
1703
 
1683
1704
  // how long generic background network is allowed to keep us waiting AFTER
@@ -1764,6 +1785,19 @@ async function waitForSootsimIdle({
1764
1785
  Date.now() - visuallyStillSince >= requiredStillMs &&
1765
1786
  (networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
1766
1787
  ) {
1788
+ if (transitionWait) {
1789
+ const started = await transitionWait
1790
+ transitionWait = null
1791
+ if (started) {
1792
+ lastLayout = ''
1793
+ lastRequestTotal = -1
1794
+ lastAppCommits = null
1795
+ lastNodeCount = null
1796
+ stable = 0
1797
+ visuallyStillSince = 0
1798
+ continue
1799
+ }
1800
+ }
1767
1801
  return { settled: true, elapsed: Date.now() - start, blockedBy: '' }
1768
1802
  }
1769
1803
  } else {
@@ -1782,7 +1816,7 @@ async function waitForSootsimIdle({
1782
1816
  const blockers = []
1783
1817
  if (strict && !last.renderStatsAvailable) blockers.push('render stats unavailable')
1784
1818
  if (last.animating) blockers.push('animations still running')
1785
- if (last.shellAnimating) blockers.push('shell animations still running')
1819
+ if (last.shellAnimating) blockers.push('shell animations still running (' + last.shellBlockers + ')')
1786
1820
  if (last.layoutDirty) blockers.push('layout still dirty')
1787
1821
  if (last.appCommits !== null && lastAppCommits !== null && last.appCommits !== lastAppCommits) blockers.push('layout commits still landing')
1788
1822
  if (last.nodeCount !== null && lastNodeCount !== null && last.nodeCount !== lastNodeCount) blockers.push('node count still changing')
@@ -1813,12 +1847,7 @@ async function waitForSootsimIdle({
1813
1847
  }
1814
1848
  );
1815
1849
  } catch (error) {
1816
- const message = error instanceof Error ? error.message : String(error);
1817
- return {
1818
- settled: false,
1819
- elapsed: maxMs,
1820
- blockedBy: message
1821
- };
1850
+ throw new IdleObservationError(Math.max(0, Date.now() - observationStartedAt), error);
1822
1851
  }
1823
1852
  if (result?.error === "runtime_readiness_unsupported" && typeof result.runtimeVersion === "string" && Array.isArray(result.missingFields) && result.missingFields.every((field) => typeof field === "string")) {
1824
1853
  throw new RuntimeReadinessUnsupportedError(
@@ -2453,7 +2482,12 @@ var SimClient = class {
2453
2482
  if (!status.ready) throw new SimReadyTimeoutError(status);
2454
2483
  return status;
2455
2484
  }
2456
- /** wait for layout/animation/transition settle after an interaction. */
2485
+ /**
2486
+ * wait for layout/animation/transition settle after an interaction.
2487
+ * a still-busy guest returns settled false with its own elapsed time.
2488
+ * a failed idle read throws IdleObservationError with the host elapsed
2489
+ * time and the original failure.
2490
+ */
2457
2491
  settle(opts = {}) {
2458
2492
  return waitForSootsimIdle({
2459
2493
  bridge: this,
@@ -2724,9 +2758,11 @@ var SimClient = class {
2724
2758
  };
2725
2759
  export {
2726
2760
  DEBUG_CHANNELS,
2761
+ IdleObservationError,
2727
2762
  LAYOUT_GET_NATIVE_EVAL,
2728
2763
  MERGED_CONSOLE_COUNT_EVAL,
2729
2764
  NOISY_TIMELINE_KINDS,
2765
+ RuntimeReadinessUnsupportedError,
2730
2766
  SimClient,
2731
2767
  SimClientError,
2732
2768
  SimConnectError,