peachsim 0.1.939 → 0.1.1399

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 (396) hide show
  1. package/README.md +28 -22
  2. package/cli/app-catalog.ts +29 -14
  3. package/cli/app-fonts.ts +5 -3
  4. package/cli/bridge-flow-runner.ts +40 -29
  5. package/cli/cloud-client.ts +57 -21
  6. package/cli/cloud-test-tunnel.ts +12 -18
  7. package/cli/commands/agent.ts +1 -1
  8. package/cli/commands/app-fonts.ts +3 -3
  9. package/cli/commands/assert.ts +8 -8
  10. package/cli/commands/auth.ts +8 -3
  11. package/cli/commands/box/bundle-plane.ts +13 -1
  12. package/cli/commands/box.ts +16 -5
  13. package/cli/commands/compat.ts +1 -1
  14. package/cli/commands/config.ts +2 -2
  15. package/cli/commands/control.ts +125 -71
  16. package/cli/commands/cpu-profile.ts +45 -1
  17. package/cli/commands/daemon.ts +131 -33
  18. package/cli/commands/debug.ts +24 -17
  19. package/cli/commands/desktop.ts +7 -1
  20. package/cli/commands/detect.ts +101 -0
  21. package/cli/commands/detox.ts +6 -6
  22. package/cli/commands/device.ts +12 -12
  23. package/cli/commands/electron.ts +32 -14
  24. package/cli/commands/flow.ts +49 -41
  25. package/cli/commands/guest-host.ts +39 -1
  26. package/cli/commands/inspect/core.ts +14 -8
  27. package/cli/commands/inspect/list.ts +1 -1
  28. package/cli/commands/inspect/settle.ts +5 -3
  29. package/cli/commands/inspect/settling.ts +229 -44
  30. package/cli/commands/inspect/wait-idle.ts +3 -2
  31. package/cli/commands/inspect.ts +105 -93
  32. package/cli/commands/install-cli.ts +36 -15
  33. package/cli/commands/install-desktop.ts +68 -195
  34. package/cli/commands/kotlin.ts +6 -5
  35. package/cli/commands/login.ts +5 -5
  36. package/cli/commands/maestro-compare.ts +554 -0
  37. package/cli/commands/maestro-generate.ts +6 -3
  38. package/cli/commands/maestro.ts +22 -0
  39. package/cli/commands/mode.ts +2 -2
  40. package/cli/commands/no-bridge-hint.ts +6 -2
  41. package/cli/commands/platform.ts +35 -6
  42. package/cli/commands/preview.ts +6 -0
  43. package/cli/commands/project-report.ts +10 -12
  44. package/cli/commands/record.ts +37 -37
  45. package/cli/commands/runtime.ts +9 -9
  46. package/cli/commands/screenshot-layers.ts +4 -4
  47. package/cli/commands/screenshot-mode.ts +2 -2
  48. package/cli/commands/screenshot.ts +8 -8
  49. package/cli/commands/screenshots-capture.ts +2 -2
  50. package/cli/commands/serve.ts +9 -12
  51. package/cli/commands/setup.ts +29 -14
  52. package/cli/commands/shell-boolean-mode.ts +2 -2
  53. package/cli/commands/show-touches-mode.ts +2 -2
  54. package/cli/commands/skills.ts +11 -11
  55. package/cli/commands/swift.ts +29 -11
  56. package/cli/commands/telemetry.ts +2 -2
  57. package/cli/commands/test.ts +5 -0
  58. package/cli/commands/three-mode.ts +3 -3
  59. package/cli/commands/timeline.ts +4 -4
  60. package/cli/commands/upgrade.ts +2 -2
  61. package/cli/commands/upload.ts +15 -15
  62. package/cli/commands/what-happened.ts +2 -2
  63. package/cli/current-sim.ts +6 -3
  64. package/cli/desktop-companion.ts +55 -149
  65. package/cli/dev-build-freshness.ts +2 -2
  66. package/cli/drivers/desktop.ts +17 -9
  67. package/cli/drivers/electron.ts +1 -2
  68. package/cli/drivers/playwright-sim-host.ts +11 -2
  69. package/cli/drivers/playwright.ts +35 -2
  70. package/cli/drivers/types.ts +8 -0
  71. package/cli/maestro-compare.ts +670 -0
  72. package/cli/outbound-endpoints.ts +46 -8
  73. package/cli/recording-access.ts +6 -6
  74. package/cli/run-registry.ts +1 -1
  75. package/cli/runtime-compat.ts +5 -1
  76. package/cli/send-to-box.ts +3 -3
  77. package/cli/shell-init.ts +2 -1
  78. package/cli/ws-bridge.ts +9 -5
  79. package/detox/element-types.ts +1 -0
  80. package/detox/gestures.ts +3 -3
  81. package/detox/index.ts +67 -38
  82. package/detox/navigation-lifecycle.ts +9 -6
  83. package/detox/proof-frame.cjs +6 -4
  84. package/dist-lib/agent-daemon-client.cjs +1 -1
  85. package/dist-lib/agent-events.cjs +1 -1
  86. package/dist-lib/attached-projects.cjs +1 -1
  87. package/dist-lib/auth/shared-session.cjs +1 -1
  88. package/dist-lib/backend-origin.cjs +1 -1
  89. package/dist-lib/beta.cjs +1 -1
  90. package/dist-lib/beta.mjs +1 -1
  91. package/dist-lib/bridge-constants.cjs +1 -1
  92. package/dist-lib/bridge-contract-input.cjs +42 -17
  93. package/dist-lib/bridge-contract-input.mjs +41 -16
  94. package/dist-lib/bridge-contract.cjs +1 -4
  95. package/dist-lib/bridge-contract.mjs +1 -3
  96. package/dist-lib/bundle-channel.cjs +2918 -550
  97. package/dist-lib/capture-contract.cjs +65 -24
  98. package/dist-lib/capture-contract.mjs +63 -24
  99. package/dist-lib/cloud-contract.cjs +2 -2
  100. package/dist-lib/cloud-contract.mjs +2 -2
  101. package/dist-lib/cloud-test-args.cjs +1 -1
  102. package/dist-lib/cloud-test-args.mjs +1 -1
  103. package/dist-lib/cloud.cjs +3 -3
  104. package/dist-lib/cloud.mjs +3 -3
  105. package/dist-lib/config.cjs +15 -5
  106. package/dist-lib/config.mjs +154 -0
  107. package/dist-lib/detox/index.cjs +495 -139
  108. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  109. package/dist-lib/expo-swift-bundle.cjs +1168 -238
  110. package/dist-lib/expo-swift-metadata.cjs +34 -23
  111. package/dist-lib/expo-swift-metadata.mjs +32 -23
  112. package/dist-lib/home-paths.cjs +2 -2
  113. package/dist-lib/host/bridge-host.cjs +2938 -654
  114. package/dist-lib/host/fetch-proxy-handler.cjs +14 -1
  115. package/dist-lib/host/fetch-proxy-overrides.cjs +7 -1
  116. package/dist-lib/host/fetch-proxy-overrides.mjs +5 -1
  117. package/dist-lib/host/replacement-module-handler.cjs +2 -2
  118. package/dist-lib/host/websocket-proxy.cjs +1 -1
  119. package/dist-lib/index.cjs +999 -353
  120. package/dist-lib/jump-to-source-babel.cjs +1 -1
  121. package/dist-lib/jump-to-source-native.cjs +1 -1
  122. package/dist-lib/kotlin-host.cjs +332 -19
  123. package/dist-lib/menu.cjs +1 -1
  124. package/dist-lib/menu.mjs +1 -1
  125. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  126. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  127. package/dist-lib/metro-production-bundle.cjs +7 -7
  128. package/dist-lib/metro-production-bundle.mjs +5 -5
  129. package/dist-lib/metro.cjs +688 -250
  130. package/dist-lib/native-app-identity-project.cjs +2647 -0
  131. package/dist-lib/native-app-identity-project.mjs +1257 -0
  132. package/dist-lib/native-app-identity.cjs +325 -25
  133. package/dist-lib/native-app-identity.mjs +323 -25
  134. package/dist-lib/native-firebase-metadata.cjs +1 -1
  135. package/dist-lib/native-firebase-metadata.mjs +1 -1
  136. package/dist-lib/plist.cjs +1 -1
  137. package/dist-lib/plist.mjs +1 -1
  138. package/dist-lib/profiles.cjs +1 -1
  139. package/dist-lib/public-brand.cjs +1 -1
  140. package/dist-lib/react-native-host-modules.cjs +5 -1
  141. package/dist-lib/react-native-host-modules.mjs +5 -1
  142. package/dist-lib/scripts/dev-server-scanner.cjs +57 -32
  143. package/dist-lib/sdk.cjs +231 -51
  144. package/dist-lib/sdk.mjs +231 -51
  145. package/dist-lib/skills.cjs +4107 -1374
  146. package/dist-lib/swift-compile.cjs +290 -30
  147. package/dist-lib/swift-compile.mjs +290 -30
  148. package/dist-lib/swift-package.cjs +143 -12
  149. package/dist-lib/swift-package.mjs +137 -11
  150. package/dist-lib/swift-submit.cjs +47 -5
  151. package/dist-lib/swift-submit.mjs +42 -5
  152. package/dist-lib/swift-toolchain.cjs +7 -2
  153. package/dist-lib/swift.cjs +1 -1
  154. package/dist-lib/vite-swift.cjs +1392 -458
  155. package/dist-lib/vite.cjs +13 -17
  156. package/dist-lib/worklet-executable.cjs +248 -33
  157. package/dist-lib/worklet-executable.mjs +248 -33
  158. package/install.sh +10 -1
  159. package/kotlin/{AppSources → AndroidViewSources}/android/content/res/Resources.kt +9 -9
  160. package/kotlin/AndroidViewSources/android/graphics/Canvas.kt +20 -0
  161. package/kotlin/AndroidViewSources/android/view/View.kt +211 -0
  162. package/kotlin/AndroidViewSources/android/widget/FrameLayout.kt +61 -0
  163. package/kotlin/AndroidViewSources/android/widget/LinearLayout.kt +49 -0
  164. package/kotlin/AppSources/android/view/Window.kt +36 -0
  165. package/kotlin/AppSources/android/view/WindowInsets.kt +9 -0
  166. package/kotlin/AppSources/android/view/WindowInsetsController.kt +5 -0
  167. package/kotlin/AppSources/android/window/OnBackInvokedDispatcher.kt +48 -0
  168. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +20 -5
  169. package/kotlin/AppSources/androidx/compose/foundation/layout/WindowInsets.kt +15 -0
  170. package/kotlin/AppSources/androidx/compose/ui/platform/ComposeView.kt +2 -1
  171. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +42 -50
  172. package/kotlin/ComposeSources/PeachComposeHost.kt +200 -84
  173. package/kotlin/ExpoComposeSources/PeachAndroidView.kt +87 -0
  174. package/kotlin/ExpoComposeSources/PeachExpoViewMount.kt +24 -0
  175. package/kotlin/ExpoComposeSources/dev/peach/expo/PeachExpoCompose.kt +28 -0
  176. package/kotlin/ExpoComposeSources/expo/modules/kotlin/views/ExpoComposeView.kt +70 -0
  177. package/kotlin/ExpoSources/dev/peach/expo/PeachExpoRuntime.kt +513 -0
  178. package/kotlin/ExpoSources/dev/peach/expo/PeachExpoTypes.kt +173 -0
  179. package/kotlin/ExpoSources/expo/modules/kotlin/AppContext.kt +20 -0
  180. package/kotlin/ExpoSources/expo/modules/kotlin/Promise.kt +19 -0
  181. package/kotlin/ExpoSources/expo/modules/kotlin/classcomponent/ClassComponentBuilder.kt +198 -0
  182. package/kotlin/ExpoSources/expo/modules/kotlin/exception/CodedException.kt +126 -0
  183. package/kotlin/ExpoSources/expo/modules/kotlin/functions/Functions.kt +132 -0
  184. package/kotlin/ExpoSources/expo/modules/kotlin/jni/JavaScriptTypedArray.kt +81 -0
  185. package/kotlin/ExpoSources/expo/modules/kotlin/modules/Module.kt +98 -0
  186. package/kotlin/ExpoSources/expo/modules/kotlin/objects/ObjectDefinitionBuilder.kt +236 -0
  187. package/kotlin/ExpoSources/expo/modules/kotlin/records/Record.kt +12 -0
  188. package/kotlin/ExpoSources/expo/modules/kotlin/runtime/Runtime.kt +9 -0
  189. package/kotlin/ExpoSources/expo/modules/kotlin/sharedobjects/SharedObject.kt +50 -0
  190. package/kotlin/ExpoSources/expo/modules/kotlin/sharedobjects/SharedObjectRegistry.kt +57 -0
  191. package/kotlin/ExpoSources/expo/modules/kotlin/sharedobjects/SharedRef.kt +19 -0
  192. package/kotlin/ExpoSources/expo/modules/kotlin/typedarray/ConcreteTypedArrays.kt +158 -0
  193. package/kotlin/ExpoSources/expo/modules/kotlin/typedarray/GenericTypedArray.kt +8 -0
  194. package/kotlin/ExpoSources/expo/modules/kotlin/typedarray/TypedArray.kt +29 -0
  195. package/kotlin/ExpoSources/expo/modules/kotlin/typedarray/TypedArrayIterator.kt +9 -0
  196. package/kotlin/ExpoSources/expo/modules/kotlin/types/AnyType.kt +155 -0
  197. package/kotlin/ExpoSources/expo/modules/kotlin/types/Either.kt +91 -0
  198. package/kotlin/ExpoSources/expo/modules/kotlin/types/Enumerable.kt +6 -0
  199. package/kotlin/ExpoSources/expo/modules/kotlin/viewevent/ViewEvent.kt +54 -0
  200. package/kotlin/ExpoSources/expo/modules/kotlin/views/ExpoView.kt +23 -0
  201. package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewDefinitionBuilder.kt +71 -0
  202. package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewProps.kt +35 -0
  203. package/kotlin/README.md +13 -3
  204. package/kotlin/Sources/android/os/Build.kt +17 -0
  205. package/kotlin/Sources/android/os/Handler.kt +35 -0
  206. package/kotlin/Sources/android/os/Looper.kt +17 -0
  207. package/kotlin/Sources/android/os/MessageQueue.kt +56 -0
  208. package/kotlin/Sources/android/os/SystemClock.kt +14 -0
  209. package/kotlin/Sources/android/util/Log.kt +64 -0
  210. package/kotlin/Sources/java/security/MessageDigest.kt +114 -0
  211. package/kotlin/Sources/kotlin/text/StringFormat.kt +9 -0
  212. package/kotlin/Sources/org/json/JSON.kt +44 -0
  213. package/kotlin/fixtures/counter/Counter.d.kt.ts +1 -0
  214. package/kotlin/fixtures/counter/Counter.kt +10 -3
  215. package/kotlin/fixtures/counter/CounterApp.native.tsx +1 -1
  216. package/kotlin/fixtures/counter/vite.config.ts +2 -2
  217. package/kotlin/fixtures/headless-counter/Headless.kt +7 -6
  218. package/kotlin/fixtures/import-animation/Animation.d.kt.ts +7 -0
  219. package/kotlin/fixtures/import-animation/Animation.kt +80 -0
  220. package/kotlin/fixtures/import-animation/ImportAnimation.native.tsx +13 -0
  221. package/kotlin/fixtures/import-animation/README.md +24 -0
  222. package/kotlin/fixtures/import-animation/app/_layout.tsx +4 -0
  223. package/kotlin/fixtures/import-animation/app/index+spa.tsx +1 -0
  224. package/kotlin/fixtures/import-animation/app/routes.d.ts +18 -0
  225. package/kotlin/fixtures/import-animation/package.json +23 -0
  226. package/kotlin/fixtures/import-animation/tsconfig.json +20 -0
  227. package/kotlin/fixtures/import-animation/vite.config.ts +20 -0
  228. package/kotlin/fixtures/import-editor/Editor.d.kt.ts +7 -0
  229. package/kotlin/fixtures/import-editor/Editor.kt +59 -0
  230. package/kotlin/fixtures/import-editor/ImportEditor.native.tsx +23 -0
  231. package/kotlin/fixtures/import-editor/README.md +27 -0
  232. package/kotlin/fixtures/import-editor/app/_layout.tsx +4 -0
  233. package/kotlin/fixtures/import-editor/app/index+spa.tsx +1 -0
  234. package/kotlin/fixtures/import-editor/package.json +23 -0
  235. package/kotlin/fixtures/import-editor/tsconfig.json +20 -0
  236. package/kotlin/fixtures/import-editor/vite.config.ts +20 -0
  237. package/kotlin/fixtures/import-effects/Effects.d.kt.ts +7 -0
  238. package/kotlin/fixtures/import-effects/Effects.kt +87 -0
  239. package/kotlin/fixtures/import-effects/ImportEffects.native.tsx +13 -0
  240. package/kotlin/fixtures/import-effects/README.md +19 -0
  241. package/kotlin/fixtures/import-effects/app/_layout.tsx +4 -0
  242. package/kotlin/fixtures/import-effects/app/index+spa.tsx +1 -0
  243. package/kotlin/fixtures/import-effects/package.json +23 -0
  244. package/kotlin/fixtures/import-effects/tsconfig.json +20 -0
  245. package/kotlin/fixtures/import-effects/vite.config.ts +20 -0
  246. package/kotlin/fixtures/import-layout/ImportLayout.native.tsx +13 -0
  247. package/kotlin/fixtures/import-layout/Layout.d.kt.ts +7 -0
  248. package/kotlin/fixtures/import-layout/Layout.kt +72 -0
  249. package/kotlin/fixtures/import-layout/README.md +24 -0
  250. package/kotlin/fixtures/import-layout/app/_layout.tsx +4 -0
  251. package/kotlin/fixtures/import-layout/app/index+spa.tsx +1 -0
  252. package/kotlin/fixtures/import-layout/package.json +23 -0
  253. package/kotlin/fixtures/import-layout/tsconfig.json +20 -0
  254. package/kotlin/fixtures/import-layout/vite.config.ts +20 -0
  255. package/kotlin/fixtures/import-navigation/ImportNavigation.native.tsx +18 -0
  256. package/kotlin/fixtures/import-navigation/Navigation.d.kt.ts +7 -0
  257. package/kotlin/fixtures/import-navigation/Navigation.kt +104 -0
  258. package/kotlin/fixtures/import-navigation/README.md +31 -0
  259. package/kotlin/fixtures/import-navigation/app/_layout.tsx +4 -0
  260. package/kotlin/fixtures/import-navigation/app/index+spa.tsx +1 -0
  261. package/kotlin/fixtures/import-navigation/package.json +23 -0
  262. package/kotlin/fixtures/import-navigation/tsconfig.json +20 -0
  263. package/kotlin/fixtures/import-navigation/vite.config.ts +20 -0
  264. package/kotlin/fixtures/import-overlays/ImportOverlays.native.tsx +29 -0
  265. package/kotlin/fixtures/import-overlays/Overlays.d.kt.ts +7 -0
  266. package/kotlin/fixtures/import-overlays/Overlays.kt +93 -0
  267. package/kotlin/fixtures/import-overlays/README.md +34 -0
  268. package/kotlin/fixtures/import-overlays/app/_layout.tsx +4 -0
  269. package/kotlin/fixtures/import-overlays/app/index+spa.tsx +1 -0
  270. package/kotlin/fixtures/import-overlays/package.json +23 -0
  271. package/kotlin/fixtures/import-overlays/tsconfig.json +20 -0
  272. package/kotlin/fixtures/import-overlays/vite.config.ts +20 -0
  273. package/kotlin/fixtures/import-scroll/ImportScroll.native.tsx +13 -0
  274. package/kotlin/fixtures/import-scroll/README.md +23 -0
  275. package/kotlin/fixtures/import-scroll/Scroll.d.kt.ts +7 -0
  276. package/kotlin/fixtures/import-scroll/Scroll.kt +91 -0
  277. package/kotlin/fixtures/import-scroll/app/_layout.tsx +4 -0
  278. package/kotlin/fixtures/import-scroll/app/index+spa.tsx +1 -0
  279. package/kotlin/fixtures/import-scroll/package.json +23 -0
  280. package/kotlin/fixtures/import-scroll/tsconfig.json +20 -0
  281. package/kotlin/fixtures/import-scroll/vite.config.ts +20 -0
  282. package/kotlin/fixtures/import-selection/ImportSelection.native.tsx +44 -0
  283. package/kotlin/fixtures/import-selection/README.md +32 -0
  284. package/kotlin/fixtures/import-selection/Selection.d.kt.ts +8 -0
  285. package/kotlin/fixtures/import-selection/Selection.kt +78 -0
  286. package/kotlin/fixtures/import-selection/app/_layout.tsx +4 -0
  287. package/kotlin/fixtures/import-selection/app/index+spa.tsx +1 -0
  288. package/kotlin/fixtures/import-selection/package.json +23 -0
  289. package/kotlin/fixtures/import-selection/tsconfig.json +20 -0
  290. package/kotlin/fixtures/import-selection/vite.config.ts +20 -0
  291. package/kotlin/fixtures/import-surfaces/ImportSurfaces.native.tsx +13 -0
  292. package/kotlin/fixtures/import-surfaces/README.md +22 -0
  293. package/kotlin/fixtures/import-surfaces/Surfaces.d.kt.ts +7 -0
  294. package/kotlin/fixtures/import-surfaces/Surfaces.kt +64 -0
  295. package/kotlin/fixtures/import-surfaces/app/_layout.tsx +4 -0
  296. package/kotlin/fixtures/import-surfaces/app/index+spa.tsx +1 -0
  297. package/kotlin/fixtures/import-surfaces/package.json +23 -0
  298. package/kotlin/fixtures/import-surfaces/tsconfig.json +20 -0
  299. package/kotlin/fixtures/import-surfaces/vite.config.ts +20 -0
  300. package/kotlin/fixtures/import-values/ImportValues.native.tsx +35 -0
  301. package/kotlin/fixtures/import-values/README.md +23 -0
  302. package/kotlin/fixtures/import-values/Values.d.kt.ts +11 -0
  303. package/kotlin/fixtures/import-values/Values.kt +66 -0
  304. package/kotlin/fixtures/import-values/app/_layout.tsx +4 -0
  305. package/kotlin/fixtures/import-values/app/index+spa.tsx +1 -0
  306. package/kotlin/fixtures/import-values/package.json +23 -0
  307. package/kotlin/fixtures/import-values/tsconfig.json +20 -0
  308. package/kotlin/fixtures/import-values/vite.config.ts +20 -0
  309. package/kotlin/fixtures/kotlin-app-variants/BarBehavior.kt +38 -0
  310. package/kotlin/fixtures/kotlin-app-variants/BarVisibility.kt +36 -0
  311. package/kotlin/fixtures/kotlin-app-variants/Ime.kt +3 -1
  312. package/kotlin/fixtures/kotlin-app-variants/TransientInsets.kt +50 -0
  313. package/kotlin/fork/NOTICE +6 -1
  314. package/kotlin/fork/README.md +15 -7
  315. package/kotlin/fork/build.sh +2 -0
  316. package/kotlin/fork/compose-worker-globals.patch +186 -4
  317. package/kotlin/fork/fork.json +2 -2
  318. package/kotlin/fork/skiko-worker-clock.patch +12 -0
  319. package/kotlin/toolchain.json +12 -12
  320. package/kotlin/tools/expo-types.main.kts +115 -0
  321. package/package.json +17 -23
  322. package/scripts/dev-server-scanner.ts +92 -66
  323. package/skills/contrast/SKILL.md +2 -2
  324. package/skills/peach-debug/SKILL.md +1 -1
  325. package/skills/peach-perf/SKILL.md +1 -1
  326. package/skills/peach-setup/SKILL.md +2 -1
  327. package/skills/peach-test/SKILL.md +8 -0
  328. package/src/assetCatalogContract.ts +43 -1
  329. package/src/bridge-contract-input.ts +46 -19
  330. package/src/bridge-contract.ts +0 -1
  331. package/src/bundle-channel.mjs +197 -84
  332. package/src/bundleGlobalFooter.ts +38 -0
  333. package/src/capture-contract.ts +107 -33
  334. package/src/cloud-contract.ts +5 -1
  335. package/src/cloud.ts +5 -2
  336. package/src/config.ts +22 -3
  337. package/src/configured-bundle-producer.ts +1 -1
  338. package/src/desktop-host-delivery.ts +95 -1
  339. package/src/expo-kotlin-bundle.ts +29 -0
  340. package/src/expo-kotlin-host.ts +201 -0
  341. package/src/expo-swift-host.ts +96 -76
  342. package/src/expoKotlinContract.ts +70 -0
  343. package/src/expoSwiftContract.ts +4 -25
  344. package/src/home-paths.ts +7 -4
  345. package/src/host/bridge-host.ts +23 -3
  346. package/src/host/fetch-proxy-handler.ts +13 -0
  347. package/src/host/fetch-proxy-overrides.ts +11 -0
  348. package/src/host/replacement-module-handler.ts +5 -2
  349. package/src/index.ts +3 -1
  350. package/src/kotlin-host.ts +463 -19
  351. package/src/metro-plugin.ts +56 -28
  352. package/src/metro-production-bundle.ts +2 -2
  353. package/src/native-app-identity-project.ts +190 -0
  354. package/src/native-app-identity.ts +398 -34
  355. package/src/native-firebase-config.ts +69 -15
  356. package/src/native-seam-manifest.ts +6 -0
  357. package/src/one-widget-discovery.ts +2 -1
  358. package/src/openstep-plist.ts +107 -0
  359. package/src/react-native-host-modules.ts +4 -0
  360. package/src/runtime-assets.ts +2 -2
  361. package/src/sim-client.ts +2 -2
  362. package/src/swift-binary-modules.ts +3 -1
  363. package/src/swift-compile.ts +286 -38
  364. package/src/swift-distribution-contract.ts +4 -0
  365. package/src/swift-distribution.lock.json +123 -12
  366. package/src/swift-distribution.ts +7 -3
  367. package/src/swift-host.ts +298 -83
  368. package/src/swift-package-manifest.ts +29 -2
  369. package/src/swift-package.ts +180 -10
  370. package/src/swift-packages.ts +4 -0
  371. package/src/swift-standin-emit.ts +4 -1
  372. package/src/swift-standins.ts +274 -15
  373. package/src/swift-submit.ts +62 -7
  374. package/src/swift-toolchain.ts +6 -1
  375. package/src/swiftContract.ts +126 -3
  376. package/src/vite-plugin-one.ts +13 -16
  377. package/src/vite-plugin-swift.ts +4 -5
  378. package/src/vite-plugin.ts +360 -144
  379. package/src/vite.d.ts +1 -1
  380. package/src/worker-system-chunks.ts +221 -0
  381. package/src/worklet-executable.ts +348 -38
  382. package/src/worklets-babel.ts +1 -1
  383. package/src/xcode-project.ts +243 -127
  384. package/dist-lib/agent-identity.cjs +0 -39
  385. package/dist-lib/agent-sessions.cjs +0 -760
  386. package/dist-lib/cli-constants.cjs +0 -32
  387. package/dist-lib/render-mode.cjs +0 -44
  388. package/kotlin/AppSources/android/view/View.kt +0 -74
  389. package/kotlin/AppSources/android/widget/FrameLayout.kt +0 -6
  390. package/src/render-mode.ts +0 -15
  391. /package/kotlin/{AppSources → AndroidViewSources}/android/content/Context.kt +0 -0
  392. /package/kotlin/{AppSources → AndroidViewSources}/android/content/res/Configuration.kt +0 -0
  393. /package/kotlin/{AppSources → AndroidViewSources}/android/graphics/Color.kt +0 -0
  394. /package/kotlin/{AppSources → AndroidViewSources}/android/util/DisplayMetrics.kt +0 -0
  395. /package/kotlin/{AppSources → AndroidViewSources}/dev/peach/app/JavaFormat.kt +0 -0
  396. /package/kotlin/{AppSources → Sources}/java/lang/Runnable.kt +0 -0
@@ -13,7 +13,6 @@
13
13
 
14
14
  import {
15
15
  HEADLESS_TENANT_FETCH_HUNG_MARKER,
16
- PUBLIC_NO_NETWORK_MARKER,
17
16
  readyProbeIsReady,
18
17
  type SemanticWaitResult,
19
18
  type WsBridgeCommand,
@@ -1197,10 +1196,7 @@ export function waitReadyReason(
1197
1196
  > & { bridgeError?: string },
1198
1197
  ): string {
1199
1198
  if (status.bridgeError) {
1200
- if (
1201
- status.bridgeError.includes(HEADLESS_TENANT_FETCH_HUNG_MARKER) ||
1202
- status.bridgeError.includes(PUBLIC_NO_NETWORK_MARKER)
1203
- ) {
1199
+ if (status.bridgeError.includes(HEADLESS_TENANT_FETCH_HUNG_MARKER)) {
1204
1200
  return status.bridgeError
1205
1201
  }
1206
1202
  return `sim unreachable: ${status.bridgeError} — the target tab is closed or no sim is connected (check \`peach list\`, reopen with \`peach open\`)`
@@ -1269,7 +1265,7 @@ export async function inspectCaptureReady(
1269
1265
  }
1270
1266
 
1271
1267
  // how long one snapshot request may take to cross the bridge and read once
1272
- const READY_SNAPSHOT_SEND_TIMEOUT_MS = 2_000
1268
+ const READY_SNAPSHOT_SEND_TIMEOUT_MS = 15_000
1273
1269
 
1274
1270
  // one readiness probe, judged on its own: no stability window, since the
1275
1271
  // caller has already let the app settle. any runtime answers it: a wait that
@@ -1286,8 +1282,18 @@ export async function inspectReadySnapshot(
1286
1282
  const deadline = Date.now() + budgetMs
1287
1283
  let result: SemanticWaitResult
1288
1284
  do {
1289
- result = await sendReadyWait(bridge, 1, READY_SNAPSHOT_SEND_TIMEOUT_MS, simId)
1290
- } while (result.polls !== 1 && !result.error && Date.now() < deadline)
1285
+ const remaining = Math.max(0, deadline - Date.now())
1286
+ const sendTimeoutMs = Math.max(
1287
+ READY_SNAPSHOT_SEND_TIMEOUT_MS,
1288
+ Math.min(remaining, 60_000),
1289
+ )
1290
+ result = await sendReadyWait(bridge, 1, sendTimeoutMs, simId)
1291
+ } while (
1292
+ result.polls !== 1 &&
1293
+ !result.error?.startsWith('sim disconnected') &&
1294
+ !result.error?.startsWith('bridge never reconnected') &&
1295
+ Date.now() < deadline
1296
+ )
1291
1297
  return waitReadyStatus(
1292
1298
  result,
1293
1299
  result.polls === 1 && result.probe ? readyProbeIsReady(result.probe) : false,
@@ -154,7 +154,7 @@ function printConnectedSims(sims: BridgeSimInfo[], selectedId?: string) {
154
154
  // classify which engine a sim is running so dev-vs-published is never ambiguous
155
155
  // to agents or humans reading `peach list`. dev shell serves local source on
156
156
  // :5173; the Contrast app embeds peach on :3000; the bridge/daemon and the
157
- // /__soot plugin serve the active runtime dir — which, inside a dev checkout, is
157
+ // /__peachsim plugin serve the active runtime dir — which, inside a dev checkout, is
158
158
  // the fresh dev-stack build (resolveDevCheckoutRuntimeRoot mode boundary), not a
159
159
  // published runtime. keep this label honest so it never says "published" while
160
160
  // actually serving local build output.
@@ -1,9 +1,11 @@
1
1
  import { waitForSootsimIdle } from './settling'
2
2
  import type { WsBridge } from '../../ws-bridge'
3
3
 
4
- // default: layout hash stable for 3 consecutive polls. --strict additionally
5
- // requires worker queryStats animation flags to be false — use when the
6
- // target app has no perpetual background animation.
4
+ // default: worker stillness plus mounted-root membership; budgets of a
5
+ // second or more also confirm the mounted geometry. --strict additionally
6
+ // requires shell motion to clear and rejects runtimes without blocking
7
+ // animation stats — use when the target app has no perpetual background
8
+ // animation.
7
9
  export async function runSettleSubcommand(opts: {
8
10
  bridge: WsBridge
9
11
  args: string[]
@@ -30,6 +30,20 @@ export type WaitForSootsimIdleResult = {
30
30
  blockedBy: string
31
31
  }
32
32
 
33
+ export class RuntimeReadinessUnsupportedError extends Error {
34
+ readonly code = 'runtime_readiness_unsupported'
35
+
36
+ constructor(
37
+ readonly runtimeVersion: string,
38
+ readonly missingFields: string[],
39
+ ) {
40
+ super(
41
+ `runtime ${runtimeVersion} cannot report animation readiness (${missingFields.join(', ')} missing); run \`peach upgrade\` and reopen the simulator`,
42
+ )
43
+ this.name = 'RuntimeReadinessUnsupportedError'
44
+ }
45
+ }
46
+
33
47
  export async function waitForSootsimIdle({
34
48
  bridge,
35
49
  simId,
@@ -39,16 +53,23 @@ export async function waitForSootsimIdle({
39
53
  strict = false,
40
54
  paintStill = false,
41
55
  }: WaitForSootsimIdleOptions): Promise<WaitForSootsimIdleResult> {
42
- const result = await bridge.send(
43
- {
44
- type: 'evaluate',
45
- simId,
46
- code: `(async () => {
56
+ let result: any
57
+ try {
58
+ result = await bridge.send(
59
+ {
60
+ type: 'evaluate',
61
+ simId,
62
+ code: `(async () => {
47
63
  const start = Date.now()
48
64
  const deadline = start + ${Math.max(0, Math.round(maxMs))}
49
65
  const pollMs = ${Math.max(1, Math.round(pollMs))}
50
66
  const strict = ${strict ? 'true' : 'false'}
51
67
  const paintStill = ${paintStill ? 'true' : 'false'}
68
+ // full mounted-geometry observation needs at least two hierarchy reads
69
+ // plus the stillness window, which a sub-second budget cannot afford:
70
+ // tight budgets (auto-wait) settle on worker stillness plus root
71
+ // membership, generous ones confirm the geometry too.
72
+ const fullGeometry = ${maxMs >= 1000 ? 'true' : 'false'}
52
73
  const requiredStillMs = paintStill ? 0 : ${Math.max(1, Math.round(stablePolls)) * Math.max(1, Math.round(pollMs))}
53
74
  const minimumStablePolls = paintStill ? 1 : ${Math.min(2, Math.max(1, Math.round(stablePolls)))}
54
75
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
@@ -85,58 +106,177 @@ export async function waitForSootsimIdle({
85
106
  }
86
107
  } catch {}
87
108
 
109
+ // a worker that cannot answer within the remaining budget is busy, not
110
+ // idle: its poll reads as unsettled. bounding each read by the budget
111
+ // keeps a busy worker from holding a poll past the deadline.
112
+ const BUSY = { busy: true }
113
+ const answer = (read) =>
114
+ Promise.race([
115
+ Promise.resolve().then(read),
116
+ new Promise((resolve) =>
117
+ setTimeout(() => resolve(BUSY), Math.max(deadline - Date.now(), 100)),
118
+ ),
119
+ ])
120
+
88
121
  const readSnapshot = async () => {
89
122
  let animating = false
90
123
  let layoutDirty = false
91
124
  let pendingFetches = 0
125
+ let workerBusy = false
92
126
  let requestTotal = 0
93
127
  let requestInFlight = 0
128
+ let appCommits = null
129
+ let nodeCount = null
94
130
  let renderStatsAvailable = false
95
131
  let shellAnimating = false
132
+ let webViewStarting = false
133
+ let webViewBlocker = ''
96
134
  // strict also waits out shell motion: a system alert, sheet or menu
97
135
  // animates in the shell worker while the app's own stats read still,
98
136
  // so its presentation is complete only once the shell's list empties.
99
137
  if (strict) {
100
- try {
101
- const motion = await window.__sootsimTest?.listAnimations?.()
102
- shellAnimating = Array.isArray(motion) && motion.some((anim) => anim.realm === 'shell')
103
- } catch {}
138
+ const motion = await answer(() => window.__sootsimTest?.listAnimations?.()).catch(() => null)
139
+ if (motion === BUSY) workerBusy = true
140
+ else shellAnimating = Array.isArray(motion) && motion.some((anim) => anim.realm === 'shell' && anim.priority !== 'low')
104
141
  }
105
- try {
106
- const stats = await window.__sootsimRenderHost?.queryStats?.()
107
- if (stats) {
108
- renderStatsAvailable = true
109
- animating =
110
- stats.hasActiveAnims === true ||
111
- stats.hasActiveNativeAnimations === true ||
112
- stats.hasPendingAnimationFrames === true
113
- layoutDirty =
114
- stats.layoutDirty === true && stats.renderRequested === true
115
- // a freshly-pushed screen showing a skeleton is layout-stable
116
- // but not actually settled — its data/images are still in
117
- // flight. treat bounded image-loader fetches as not-idle so
118
- // settle waits for real content, capped by the budget.
119
- const pf = stats.memory && stats.memory.imageLoader
120
- ? stats.memory.imageLoader.pendingFetches
121
- : 0
122
- pendingFetches = typeof pf === 'number' ? pf : 0
142
+ const stats = await answer(() => window.__sootsimRenderHost?.queryStats?.()).catch(() => null)
143
+ if (stats === BUSY) {
144
+ workerBusy = true
145
+ } else if (stats) {
146
+ renderStatsAvailable = true
147
+ const missingFields = ['hasActiveBlockingNativeAnimations', 'hasPendingBlockingAnimationFrames']
148
+ .filter((field) => typeof stats[field] !== 'boolean')
149
+ if (strict && missingFields.length > 0) {
150
+ return {
151
+ runtimeUnsupported: {
152
+ error: 'runtime_readiness_unsupported',
153
+ runtimeVersion: window.__sootsimRuntimeVersion || 'unknown',
154
+ missingFields,
155
+ },
156
+ }
123
157
  }
124
- } catch {}
158
+ // positive checks only: an older runtime without the blocking
159
+ // fields reads as still, and strict rejects that runtime above
160
+ // instead of ever settling on unknown motion.
161
+ animating =
162
+ stats.hasActiveAnims === true ||
163
+ stats.hasActiveBlockingNativeAnimations === true ||
164
+ stats.hasPendingBlockingAnimationFrames === true
165
+ layoutDirty =
166
+ stats.layoutDirty === true && stats.renderRequested === true
167
+ // a freshly-pushed screen showing a skeleton is layout-stable
168
+ // but not actually settled — its data/images are still in
169
+ // flight. treat bounded image-loader fetches as not-idle so
170
+ // settle waits for real content, capped by the budget.
171
+ const pf = stats.memory && stats.memory.imageLoader
172
+ ? stats.memory.imageLoader.pendingFetches
173
+ : 0
174
+ pendingFetches = typeof pf === 'number' ? pf : 0
175
+ // tenant layout commits: a screen that keeps committing is still
176
+ // mounting or remounting, even with no animation flag raised.
177
+ appCommits = typeof stats.appCommitCount === 'number' ? stats.appCommitCount : null
178
+ }
179
+ // image preparation ends in the tenant before the compositor decodes
180
+ // and realizes its pixels. both owners must finish before settling.
181
+ // the compositor rejects a stats request it is too busy to answer.
182
+ const compositor = await answer(() => window.__sootsimCompositor.getStats()).catch(() => BUSY)
183
+ if (compositor === BUSY) {
184
+ workerBusy = true
185
+ } else {
186
+ pendingFetches += compositor.images.pendingFetches
187
+ pendingFetches += compositor.imageRealization.pending
188
+ if (compositor.webViewCaptureSettled === false) {
189
+ webViewStarting = true
190
+ webViewBlocker = 'compositor webView capture not settled'
191
+ }
192
+ }
125
193
  try {
126
194
  const counts = await window.__sootsimTest?.getRequestCounts?.()
127
195
  requestTotal = typeof counts?.total === 'number' ? counts.total : 0
128
196
  requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
129
197
  } catch {}
130
- const root = window.__sootsimRoot
198
+ // mounted-root membership: the live node count moves while the tree
199
+ // mounts, unmounts or recycles, in either render mode.
200
+ try {
201
+ const total = await window.__sootsimTest?.getNodeCount?.()
202
+ nodeCount = typeof total === 'number' ? total : null
203
+ } catch {}
204
+ // the mounted hierarchy in render-worker mode comes from the test
205
+ // bridge: window.__sootsimRoot is an empty placeholder there, so a
206
+ // walk over it hashes every screen to the same empty string while
207
+ // it moves. listInspectable is the worker-backed read describe
208
+ // uses; a worker too busy to answer it reads as unsettled. tight
209
+ // budgets skip this read and settle on the cheaper signals above.
210
+ let hierarchy = null
211
+ if (fullGeometry && typeof window.__sootsimTest?.listInspectable === 'function') {
212
+ try {
213
+ hierarchy = await answer(() => window.__sootsimTest.listInspectable({}))
214
+ } catch {
215
+ hierarchy = null
216
+ }
217
+ }
218
+ if (hierarchy === BUSY) {
219
+ workerBusy = true
220
+ hierarchy = null
221
+ } else if (!Array.isArray(hierarchy)) {
222
+ hierarchy = null
223
+ }
131
224
  const nodes = []
132
- if (root) {
133
- const walk = (n) => {
134
- if (n.layout && n.layout.width > 0) {
135
- nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
225
+ const PREVIEW_PROGRESS_REGEX = /^(creating webview|loading html|fetching bundle|waiting for first render|starting preview|loading simulator|fetching app code|waiting for native preview|starting design|loading canvas|fetching screens|painting the canvas|waiting for webview|waiting for preview|waiting for canvas|preview stalled)/i
226
+ const notePreviewProgress = (tid, text) => {
227
+ if (typeof tid === 'string' && (tid === 'web-preview-progress' || tid.endsWith('-preview-progress'))) {
228
+ webViewStarting = true
229
+ if (!webViewBlocker) webViewBlocker = 'preview progress active (' + tid + ')'
230
+ }
231
+ if (typeof text === 'string' && PREVIEW_PROGRESS_REGEX.test(text.trim())) {
232
+ webViewStarting = true
233
+ if (!webViewBlocker) webViewBlocker = 'webView startup in progress ("' + text.trim() + '")'
234
+ }
235
+ }
236
+ if (hierarchy) {
237
+ for (const n of hierarchy) {
238
+ const layout = n && n.layout
239
+ if (layout && layout.width > 0) {
240
+ const abs = (n && (n.absolute || n.absolutePosition)) || layout
241
+ nodes.push(Math.round(abs.x) + ',' + Math.round(abs.y) + ',' + Math.round(layout.width))
242
+ }
243
+ notePreviewProgress(
244
+ n.testID || n.testId,
245
+ typeof n.text === 'string' ? n.text : '',
246
+ )
247
+ }
248
+ } else {
249
+ const root = window.__sootsimRoot
250
+ if (root) {
251
+ const walk = (n) => {
252
+ if (n.layout && n.layout.width > 0) {
253
+ nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
254
+ }
255
+ notePreviewProgress(
256
+ n.testID || n.props?.testID || n.props?.testId,
257
+ n.textContent || (typeof n.props?.children === 'string' ? n.props.children : ''),
258
+ )
259
+ for (const c of n.children || []) walk(c)
260
+ }
261
+ walk(root)
262
+ }
263
+ }
264
+ if (!webViewStarting && typeof document !== 'undefined') {
265
+ const progressEl = document.querySelector(
266
+ '[data-testid$="-preview-progress"], [data-testid="web-preview-progress"], [data-testid="ios-preview-progress"], [data-testid="design-preview-progress"]'
267
+ )
268
+ if (progressEl) {
269
+ webViewStarting = true
270
+ webViewBlocker = 'preview progress element displayed'
271
+ } else {
272
+ const unreadyWebView = document.querySelector(
273
+ '[data-sootsim-webview-loading="true"], [data-sootsim-webview-mode="placeholder"]'
274
+ )
275
+ if (unreadyWebView) {
276
+ webViewStarting = true
277
+ webViewBlocker = 'webView document still loading'
136
278
  }
137
- for (const c of n.children || []) walk(c)
138
279
  }
139
- walk(root)
140
280
  }
141
281
  if (paintStill) {
142
282
  const canvas = composite.getCanvas()
@@ -150,7 +290,7 @@ export async function waitForSootsimIdle({
150
290
  pixelFrameToken = token
151
291
  }
152
292
  }
153
- return { layout: nodes.join(';'), screenPixels, animating, shellAnimating, layoutDirty, pendingFetches, renderStatsAvailable, requestTotal, requestInFlight }
293
+ return { layout: nodes.join(';'), screenPixels, animating, shellAnimating, layoutDirty, pendingFetches, workerBusy, webViewStarting, webViewBlocker, renderStatsAvailable, requestTotal, requestInFlight, appCommits, nodeCount }
154
294
  }
155
295
 
156
296
  // how long generic background network is allowed to keep us waiting AFTER
@@ -168,6 +308,8 @@ export async function waitForSootsimIdle({
168
308
  let lastScreenPixels = null
169
309
  let screenChangedPolls = 0
170
310
  let lastRequestTotal = -1
311
+ let lastAppCommits = null
312
+ let lastNodeCount = null
171
313
  let stable = 0
172
314
  let visuallyStillSince = 0
173
315
  // counts across the whole budget: a flag that is true on every poll is a
@@ -177,6 +319,7 @@ export async function waitForSootsimIdle({
177
319
  let dirtyPolls = 0
178
320
  let animatingPolls = 0
179
321
  let layoutChangedPolls = 0
322
+ let webViewStartingPolls = 0
180
323
  const screenUnchanged = (pixels) => {
181
324
  if (!pixels || !lastScreenPixels || pixels.width !== lastScreenPixels.width || pixels.height !== lastScreenPixels.height) return false
182
325
  if (pixels === lastScreenPixels) return true
@@ -187,9 +330,11 @@ export async function waitForSootsimIdle({
187
330
  }
188
331
  while (Date.now() < deadline) {
189
332
  const snapshot = await readSnapshot()
333
+ if (snapshot.runtimeUnsupported) return snapshot.runtimeUnsupported
190
334
  polls++
191
335
  if (snapshot.layoutDirty) dirtyPolls++
192
336
  if (snapshot.animating || snapshot.shellAnimating) animatingPolls++
337
+ if (snapshot.webViewStarting) webViewStartingPolls++
193
338
  if (polls > 1 && snapshot.layout !== lastLayout) layoutChangedPolls++
194
339
  const screenChanged = paintStill && !screenUnchanged(snapshot.screenPixels)
195
340
  if (polls > 1 && screenChanged) screenChangedPolls++
@@ -199,12 +344,24 @@ export async function waitForSootsimIdle({
199
344
  !snapshot.animating &&
200
345
  !snapshot.shellAnimating &&
201
346
  !snapshot.layoutDirty)
202
- // visually settled: no animations, no image content loading, layout
203
- // unchanged since the last poll.
347
+ // visually settled: no worker motion or layout work, no new layout
348
+ // commits, a stable mounted node count, no image content loading,
349
+ // no webview startup in progress, layout unchanged since the last
350
+ // poll. both modes wait out finite worker motion now; strict
351
+ // additionally implies the flags through strictOk and adds shell
352
+ // motion plus the blocking-stats floor. a null count or commit
353
+ // reading means the runtime does not report it, which reads as
354
+ // stable the way a missing stats block does.
204
355
  const visuallyStill =
205
356
  strictOk &&
357
+ !snapshot.animating &&
358
+ !snapshot.layoutDirty &&
359
+ (snapshot.appCommits === null || lastAppCommits === null || snapshot.appCommits === lastAppCommits) &&
360
+ (snapshot.nodeCount === null || lastNodeCount === null || snapshot.nodeCount === lastNodeCount) &&
206
361
  !screenChanged &&
207
362
  snapshot.pendingFetches === 0 &&
363
+ !snapshot.workerBusy &&
364
+ !snapshot.webViewStarting &&
208
365
  snapshot.layout === lastLayout
209
366
  // generic network quiet: nothing in flight and no new requests issued.
210
367
  const networkQuiet =
@@ -229,15 +386,22 @@ export async function waitForSootsimIdle({
229
386
  lastLayout = snapshot.layout
230
387
  lastScreenPixels = snapshot.screenPixels
231
388
  lastRequestTotal = snapshot.requestTotal
389
+ lastAppCommits = snapshot.appCommits
390
+ lastNodeCount = snapshot.nodeCount
232
391
  await sleep(pollMs)
233
392
  }
234
393
  const last = await readSnapshot()
394
+ if (last.runtimeUnsupported) return last.runtimeUnsupported
235
395
  const blockers = []
236
396
  if (strict && !last.renderStatsAvailable) blockers.push('render stats unavailable')
237
397
  if (last.animating) blockers.push('animations still running')
238
398
  if (last.shellAnimating) blockers.push('shell animations still running')
239
399
  if (last.layoutDirty) blockers.push('layout still dirty')
400
+ if (last.appCommits !== null && lastAppCommits !== null && last.appCommits !== lastAppCommits) blockers.push('layout commits still landing')
401
+ if (last.nodeCount !== null && lastNodeCount !== null && last.nodeCount !== lastNodeCount) blockers.push('node count still changing')
240
402
  if (last.pendingFetches > 0) blockers.push(last.pendingFetches + ' image fetches pending')
403
+ if (last.workerBusy) blockers.push('a render worker too busy to report')
404
+ if (last.webViewStarting) blockers.push(last.webViewBlocker || 'webView still starting')
241
405
  if (last.layout !== lastLayout) blockers.push('layout still changing')
242
406
  if (paintStill && !last.screenPixels) blockers.push('screen pixels unavailable')
243
407
  else if (paintStill && !screenUnchanged(last.screenPixels)) blockers.push('screen pixels still changing')
@@ -250,17 +414,37 @@ export async function waitForSootsimIdle({
250
414
  blockedBy:
251
415
  (blockers.length ? blockers.join(', ') : 'unknown') +
252
416
  ' over ' + polls + ' polls (layout changed ' + layoutChangedPolls +
253
- 'x, layoutDirty ' + dirtyPolls + 'x, animating ' + animatingPolls + 'x, screen changed ' + screenChangedPolls + 'x)',
417
+ 'x, layoutDirty ' + dirtyPolls + 'x, animating ' + animatingPolls + 'x, webViewStarting ' + webViewStartingPolls + 'x, screen changed ' + screenChangedPolls + 'x)',
254
418
  }
255
419
  } finally {
256
420
  unsubscribe?.()
257
421
  }
258
422
  })()`,
259
- },
260
- {
261
- timeoutMs: maxMs + 1_000,
262
- },
263
- )
423
+ },
424
+ {
425
+ timeoutMs: Math.max(5_000, maxMs + 2_000),
426
+ },
427
+ )
428
+ } catch (error) {
429
+ const message = error instanceof Error ? error.message : String(error)
430
+ return {
431
+ settled: false,
432
+ elapsed: maxMs,
433
+ blockedBy: message,
434
+ }
435
+ }
436
+
437
+ if (
438
+ result?.error === 'runtime_readiness_unsupported' &&
439
+ typeof result.runtimeVersion === 'string' &&
440
+ Array.isArray(result.missingFields) &&
441
+ result.missingFields.every((field: unknown) => typeof field === 'string')
442
+ ) {
443
+ throw new RuntimeReadinessUnsupportedError(
444
+ result.runtimeVersion,
445
+ result.missingFields,
446
+ )
447
+ }
264
448
 
265
449
  const { elapsed, settled, blockedBy } = (result ??
266
450
  {}) as Partial<WaitForSootsimIdleResult>
@@ -299,6 +483,7 @@ export async function waitReadyAndSettle(
299
483
  })
300
484
  let idle: WaitForSootsimIdleResult | null = null
301
485
  for (;;) {
486
+ idle = null
302
487
  const ready = await inspectWaitReady(bridge, remaining(), options)
303
488
  if (!ready.ready) return result(ready, idle)
304
489
  idle = await waitForSootsimIdle({
@@ -4,8 +4,9 @@ import { waitForSootsimIdle } from './settling'
4
4
  import type { WsBridge } from '../../ws-bridge'
5
5
 
6
6
  // alias for `do settle` under the discoverable `wait` verb grouping.
7
- // layout hash stability across 3 polls; --strict also requires the app's
8
- // animation flags clear and no shell motion (alerts, sheets, menus).
7
+ // worker stillness plus mounted-root membership; budgets of a second or
8
+ // more also confirm the mounted geometry. --strict also requires shell
9
+ // motion clear (alerts, sheets, menus) and blocking animation stats.
9
10
  export async function runWaitIdleSubcommand(opts: {
10
11
  bridge: WsBridge
11
12
  args: string[]