peachsim 0.1.577 → 0.1.579

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 (178) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +11 -9
  3. package/cli/app-catalog.ts +77 -0
  4. package/cli/app-fonts.ts +4 -20
  5. package/cli/app-splash.ts +2 -18
  6. package/cli/bridge-flow-runner.ts +505 -139
  7. package/cli/cloud-client.ts +1 -4
  8. package/cli/cloud-session.ts +1 -4
  9. package/cli/cloud-test-result-files.ts +3 -0
  10. package/cli/cloud-test-tunnel.ts +2 -5
  11. package/cli/command-registry.ts +2 -0
  12. package/cli/commands/agent.ts +149 -23
  13. package/cli/commands/assert.ts +1 -4
  14. package/cli/commands/box/peach-command.ts +62 -0
  15. package/cli/commands/box/plane-share.ts +0 -70
  16. package/cli/commands/box/rnx-command.ts +1 -60
  17. package/cli/commands/box/search-command.ts +14 -0
  18. package/cli/commands/box.ts +2 -2
  19. package/cli/commands/control.ts +121 -27
  20. package/cli/commands/cpu-profile.ts +23 -10
  21. package/cli/commands/daemon.ts +1 -9
  22. package/cli/commands/detox.ts +26 -18
  23. package/cli/commands/device.ts +129 -1
  24. package/cli/commands/film.ts +25 -16
  25. package/cli/commands/flow.ts +9 -2
  26. package/cli/commands/inspect/actions.ts +75 -1
  27. package/cli/commands/inspect/core.ts +81 -2
  28. package/cli/commands/inspect/describe.ts +2 -6
  29. package/cli/commands/inspect/env.ts +1 -1
  30. package/cli/commands/inspect/settling.ts +20 -3
  31. package/cli/commands/inspect/shared.ts +1 -4
  32. package/cli/commands/inspect/sleep.ts +1 -1
  33. package/cli/commands/inspect.ts +16 -49
  34. package/cli/commands/install-desktop.ts +8 -6
  35. package/cli/commands/login.ts +3 -5
  36. package/cli/commands/maestro-generate.ts +1 -4
  37. package/cli/commands/maestro.ts +38 -21
  38. package/cli/commands/no-bridge-hint.ts +0 -14
  39. package/cli/commands/permissions.ts +2 -2
  40. package/cli/commands/platform.ts +13 -6
  41. package/cli/commands/screenshot.ts +6 -1
  42. package/cli/commands/skills.ts +1 -1
  43. package/cli/commands/test.ts +6 -2
  44. package/cli/commands/three-mode.ts +162 -19
  45. package/cli/commands/upload.ts +3 -18
  46. package/cli/current-sim.ts +0 -1
  47. package/cli/desktop-companion.ts +40 -8
  48. package/cli/drivers/playwright-provisioning.ts +1 -4
  49. package/cli/flow-export.ts +1 -8
  50. package/cli/flow-session.ts +0 -4
  51. package/cli/hints.ts +1 -16
  52. package/cli/inspect-notice-state.ts +0 -4
  53. package/cli/main.ts +2 -4
  54. package/cli/outbound-endpoints.ts +1 -24
  55. package/cli/parse-args.ts +1 -2
  56. package/cli/rnx-in-process.ts +3 -1
  57. package/cli/run-rnx-core.ts +4 -2
  58. package/cli/run-rnx.ts +18 -10
  59. package/cli/send-to-box.ts +1 -4
  60. package/cli/telemetry.ts +1 -1
  61. package/cli/ws-bridge.ts +1 -5
  62. package/detox/gestures.ts +20 -6
  63. package/detox/index.ts +379 -123
  64. package/detox/navigation-lifecycle.ts +22 -13
  65. package/dist-lib/agent-daemon-client.cjs +21 -15
  66. package/dist-lib/agent-events.cjs +1 -1
  67. package/dist-lib/agent-identity.cjs +3 -17
  68. package/dist-lib/agent-sessions.cjs +5 -4
  69. package/dist-lib/attached-projects.cjs +1 -77
  70. package/dist-lib/auth/shared-session.cjs +1 -1
  71. package/dist-lib/backend-origin.cjs +11 -1
  72. package/dist-lib/beta.cjs +1 -1
  73. package/dist-lib/beta.mjs +1 -1
  74. package/dist-lib/bridge-constants.cjs +1 -1
  75. package/dist-lib/bridge-contract-input.cjs +7 -5
  76. package/dist-lib/bridge-contract-input.mjs +7 -5
  77. package/dist-lib/bridge-contract.cjs +1 -1
  78. package/dist-lib/bridge-contract.mjs +1 -1
  79. package/dist-lib/capture-contract.cjs +30 -7
  80. package/dist-lib/capture-contract.mjs +30 -7
  81. package/dist-lib/cli-constants.cjs +1 -1
  82. package/dist-lib/cloud-contract.cjs +1 -1
  83. package/dist-lib/cloud-contract.mjs +1 -1
  84. package/dist-lib/cloud-test-args.cjs +1 -1
  85. package/dist-lib/cloud-test-args.mjs +1 -1
  86. package/dist-lib/cloud.cjs +17 -11
  87. package/dist-lib/cloud.mjs +17 -11
  88. package/dist-lib/config.cjs +1 -1
  89. package/dist-lib/detox/index.cjs +321 -122
  90. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  91. package/dist-lib/home-paths.cjs +1 -1
  92. package/dist-lib/host/bridge-host.cjs +427 -212
  93. package/dist-lib/host/fetch-proxy-handler.cjs +5 -28
  94. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  95. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  96. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  97. package/dist-lib/host/websocket-proxy.cjs +1 -1
  98. package/dist-lib/index.cjs +315 -58
  99. package/dist-lib/jump-to-source-babel.cjs +1 -1
  100. package/dist-lib/jump-to-source-native.cjs +1 -1
  101. package/dist-lib/menu.cjs +67 -31
  102. package/dist-lib/menu.mjs +64 -28
  103. package/dist-lib/metro-fingerprint-registry.cjs +8 -4
  104. package/dist-lib/metro-fingerprint-registry.mjs +6 -4
  105. package/dist-lib/metro-production-bundle.cjs +2 -2
  106. package/dist-lib/metro-production-bundle.mjs +2 -2
  107. package/dist-lib/metro.cjs +8 -4
  108. package/dist-lib/profiles.cjs +1 -1
  109. package/dist-lib/public-brand.cjs +7 -4
  110. package/dist-lib/react-native-host-modules.cjs +1 -1
  111. package/dist-lib/react-native-host-modules.mjs +1 -1
  112. package/dist-lib/render-mode.cjs +3 -13
  113. package/dist-lib/scripts/dev-server-scanner.cjs +5 -17
  114. package/dist-lib/sdk.cjs +129 -10
  115. package/dist-lib/sdk.mjs +127 -10
  116. package/dist-lib/skills.cjs +1635 -1121
  117. package/dist-lib/swift-compile.cjs +277 -70
  118. package/dist-lib/swift-compile.mjs +275 -70
  119. package/dist-lib/swift-package.cjs +17 -5
  120. package/dist-lib/swift-package.mjs +15 -4
  121. package/dist-lib/swift-submit.cjs +32 -8
  122. package/dist-lib/swift-submit.mjs +29 -7
  123. package/dist-lib/swift.cjs +30912 -577
  124. package/dist-lib/vite-kotlin.cjs +356 -0
  125. package/dist-lib/vite-swift.cjs +1743 -0
  126. package/dist-lib/vite.cjs +208 -1039
  127. package/kotlin/README.md +18 -0
  128. package/kotlin/Sources/androidx/annotation/Keep.kt +4 -0
  129. package/kotlin/Sources/dev/onejs/one/OneNativeSourceDispatch.kt +7 -0
  130. package/kotlin/Sources/dev/onejs/one/source/OneModule.kt +4 -0
  131. package/kotlin/Sources/org/json/JSON.kt +244 -0
  132. package/kotlin/toolchain.json +19 -0
  133. package/package.json +14 -2
  134. package/scripts/dev-server-scanner.ts +0 -18
  135. package/scripts/optional-demo-registry.ts +0 -17
  136. package/skills/contrast/SKILL.md +2 -2
  137. package/skills/peach-setup/SKILL.md +16 -0
  138. package/src/agent-daemon-client.ts +36 -22
  139. package/src/agent-identity.ts +0 -15
  140. package/src/agent-prompt.ts +12 -0
  141. package/src/attached-projects.ts +0 -72
  142. package/src/backend-origin.ts +10 -0
  143. package/src/bridge-contract-input.ts +2 -5
  144. package/src/bridge-contract.ts +2 -0
  145. package/src/capture-contract.ts +53 -11
  146. package/src/cloud.ts +4 -9
  147. package/src/dev-server-open.ts +0 -99
  148. package/src/home-paths.ts +2 -3
  149. package/src/host/agent-host.ts +82 -36
  150. package/src/host/bridge-host.ts +5 -11
  151. package/src/host/fetch-proxy-handler.ts +19 -43
  152. package/src/is-record.ts +4 -0
  153. package/src/menu.ts +82 -37
  154. package/src/metro-fingerprint-registry.ts +1 -4
  155. package/src/metro-production-bundle.ts +1 -1
  156. package/src/plist.ts +1 -4
  157. package/src/public-brand.ts +5 -3
  158. package/src/render-mode.ts +0 -12
  159. package/src/runtime-assets.ts +9 -2
  160. package/src/screenshots/compose.ts +7 -1
  161. package/src/screenshots/frame-compose.ts +21 -10
  162. package/src/screenshots/schema.ts +1 -4
  163. package/src/sim-client.ts +2 -4
  164. package/src/skills/builtin/maestro-test.ts +1 -4
  165. package/src/sleep.ts +4 -0
  166. package/src/swift-compile.ts +209 -47
  167. package/src/swift-icu.ts +140 -0
  168. package/src/swift-package.ts +26 -2
  169. package/src/swift-submit.ts +84 -4
  170. package/src/swift.ts +2 -0
  171. package/src/vite-plugin-kotlin.ts +376 -0
  172. package/src/vite-plugin-one.ts +224 -36
  173. package/src/vite-plugin-swift.ts +382 -96
  174. package/src/vite-plugin.ts +121 -134
  175. package/src/vite.d.ts +10 -0
  176. package/cli/resolve-assets.ts +0 -63
  177. package/src/rolldown-plugin-swift.ts +0 -244
  178. package/src/team-machine-prompt.ts +0 -73
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.577 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.579 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -11,8 +11,8 @@ var __commonJS = (cb, mod) => function __require() {
11
11
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
12
12
  };
13
13
  var __export = (target, all) => {
14
- for (var name in all)
15
- __defProp(target, name, { get: all[name], enumerable: true });
14
+ for (var name2 in all)
15
+ __defProp(target, name2, { get: all[name2], enumerable: true });
16
16
  };
17
17
  var __copyProps = (to, from, except, desc) => {
18
18
  if (from && typeof from === "object" || typeof from === "function") {
@@ -301,13 +301,13 @@ function assertGpuBrowserCommand(command, runtime = currentRuntime()) {
301
301
  `[reap-browser] Chrome process did not retain the required ${angle} GPU arguments`
302
302
  );
303
303
  }
304
- for (const name of ["--enable-features=", "--disable-features="]) {
305
- const lists = args.filter((arg) => arg.startsWith(name)).map((arg) => arg.slice(name.length).split(",").filter(Boolean));
304
+ for (const name2 of ["--enable-features=", "--disable-features="]) {
305
+ const lists = args.filter((arg) => arg.startsWith(name2)).map((arg) => arg.slice(name2.length).split(",").filter(Boolean));
306
306
  const kept = new Set(lists.at(-1) ?? []);
307
307
  const dropped = lists.slice(0, -1).flat().filter((feature) => !kept.has(feature));
308
308
  if (dropped.length > 0) {
309
309
  throw new Error(
310
- `[reap-browser] Chrome keeps only its last ${name.slice(0, -1)} switch, which dropped ${dropped.join(",")}; add them to the merged list in scripts/lib/reap-browser.ts`
310
+ `[reap-browser] Chrome keeps only its last ${name2.slice(0, -1)} switch, which dropped ${dropped.join(",")}; add them to the merged list in scripts/lib/reap-browser.ts`
311
311
  );
312
312
  }
313
313
  }
@@ -884,6 +884,19 @@ function isAppBootTargetUrl(initUrl) {
884
884
  }
885
885
  }
886
886
 
887
+ // src/public-brand.ts
888
+ var name = "Peach";
889
+ var peachPublicBrand = Object.freeze({
890
+ name,
891
+ packageName: "peachsim",
892
+ commandName: "peach",
893
+ domain: "peachsim.com",
894
+ origin: "https://peachsim.com"
895
+ });
896
+
897
+ // src/menu.ts
898
+ var SOOTSIM_SHELL_COMMAND_NAME = "sootsim:shell-command";
899
+
887
900
  // src/poll-until.ts
888
901
  async function pollUntil(read, ready, options) {
889
902
  if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
@@ -979,7 +992,14 @@ async function pollUntil(read, ready, options) {
979
992
  }
980
993
  }
981
994
 
995
+ // ../peach-engine/src/engine/scroll-platform.ts
996
+ var IOS_SCROLL_AXIS_TANGENT = Math.sqrt(3);
997
+ function scrollTouchSlop(platform, primaryDelta) {
998
+ return platform === "android" ? 0 : Math.sign(primaryDelta) * 10;
999
+ }
1000
+
982
1001
  // detox/gestures.ts
1002
+ var DETOX_PLATFORM = process.env.PEACH_PLATFORM === "android" ? "android" : "ios";
983
1003
  function resolveScrollStartPosition(value, label) {
984
1004
  const normalized = value === void 0 || Number.isNaN(value) ? 0.5 : value;
985
1005
  if (!Number.isFinite(normalized) || normalized < 0 || normalized > 1) {
@@ -1171,23 +1191,24 @@ async function dragScrollNode(page, node, pixels, direction, startPositionX, sta
1171
1191
  const startSootSimY = node.absolutePosition.y + node.layout.height * normalizedStartY;
1172
1192
  let endSootSimX = startSootSimX;
1173
1193
  let endSootSimY = startSootSimY;
1194
+ const travel = pixels + scrollTouchSlop(DETOX_PLATFORM, 1);
1174
1195
  switch (direction) {
1175
1196
  case "down":
1176
- endSootSimY -= pixels;
1197
+ endSootSimY -= travel;
1177
1198
  break;
1178
1199
  case "up":
1179
- endSootSimY += pixels;
1200
+ endSootSimY += travel;
1180
1201
  break;
1181
1202
  case "left":
1182
- endSootSimX += pixels;
1203
+ endSootSimX += travel;
1183
1204
  break;
1184
1205
  case "right":
1185
- endSootSimX -= pixels;
1206
+ endSootSimX -= travel;
1186
1207
  break;
1187
1208
  }
1188
1209
  const startPage = await sootsimToPage(page, startSootSimX, startSootSimY);
1189
1210
  const endPage = await sootsimToPage(page, endSootSimX, endSootSimY);
1190
- const steps = Math.max(16, Math.ceil(pixels / 8));
1211
+ const steps = Math.max(16, Math.ceil(travel / 8));
1191
1212
  await page.mouse.move(startPage.x, startPage.y);
1192
1213
  await page.mouse.down();
1193
1214
  await page.waitForTimeout(20);
@@ -1197,6 +1218,7 @@ async function dragScrollNode(page, node, pixels, direction, startPositionX, sta
1197
1218
  await page.mouse.move(startPage.x + dx * i, startPage.y + dy * i);
1198
1219
  await page.waitForTimeout(24);
1199
1220
  }
1221
+ await page.waitForTimeout(300);
1200
1222
  await page.mouse.up();
1201
1223
  await page.waitForTimeout(180);
1202
1224
  }
@@ -1861,17 +1883,23 @@ function attachPageDiagnostics(page) {
1861
1883
  }
1862
1884
  async function readSootsimPageState(page) {
1863
1885
  try {
1864
- return await page.evaluate(() => ({
1865
- bodyText: document.body?.innerText?.slice(0, 500) ?? "",
1866
- bridges: {
1867
- sootsim: Boolean(window.SootSim),
1868
- test: Boolean(window.__sootsimTest)
1869
- },
1870
- globals: Object.keys(window).filter((key) => key.startsWith("__sootsim") || key === "SootSim").sort(),
1871
- readyState: document.readyState,
1872
- title: document.title,
1873
- url: window.location.href
1874
- }));
1886
+ return await page.evaluate(() => {
1887
+ const bootMarks = Reflect.get(window, "__sootsimBootMarks");
1888
+ return {
1889
+ bodyText: document.body?.innerText?.slice(0, 500) ?? "",
1890
+ bootStage: document.documentElement.dataset.sootsimBootStage ?? null,
1891
+ bootMarks: Array.isArray(bootMarks) ? bootMarks.slice(-16) : [],
1892
+ capturedAtEpochMs: Date.now(),
1893
+ bridges: {
1894
+ sootsim: Boolean(window.SootSim),
1895
+ test: Boolean(window.__sootsimTest)
1896
+ },
1897
+ globals: Object.keys(window).filter((key) => key.startsWith("__sootsim") || key === "SootSim").sort(),
1898
+ readyState: document.readyState,
1899
+ title: document.title,
1900
+ url: window.location.href
1901
+ };
1902
+ });
1875
1903
  } catch (error) {
1876
1904
  return {
1877
1905
  evaluateError: error instanceof Error ? error.message : String(error)
@@ -1936,7 +1964,6 @@ function getSootsimNavigationDiagnostics(page) {
1936
1964
  var import_proof_frame = __toESM(require_proof_frame(), 1);
1937
1965
  var BASE_URL = process.env.PEACH_URL || "http://localhost:5173";
1938
1966
  var SCREENSHOT_DIR = process.env.PEACH_SCREENSHOT_DIR || path.join(process.cwd(), "test", "detox-driver", "screenshots");
1939
- var DETOX_PLATFORM = process.env.PEACH_PLATFORM === "android" ? "android" : "ios";
1940
1967
  var _browser = null;
1941
1968
  var _reapBrowserNow = null;
1942
1969
  var _context = null;
@@ -2031,85 +2058,83 @@ async function waitForSootsimSurfaceMetrics(page, timeout = 3e4) {
2031
2058
  async function waitForSootsimShellReady(page, timeout = 3e4) {
2032
2059
  const expectsInitialAppPaint = isAppBootTargetUrl(page.url());
2033
2060
  try {
2034
- await page.evaluate(async ({ timeoutMs, expectsInitialAppPaint: expectsInitialAppPaint2 }) => {
2035
- const start = Date.now();
2036
- const deadline = start + timeoutMs;
2037
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2038
- const remaining = () => Math.max(1, deadline - Date.now());
2039
- const withTimeout = async (label, promise) => {
2040
- let timer = null;
2041
- try {
2042
- await Promise.race([
2043
- promise,
2044
- new Promise((_, reject) => {
2045
- timer = setTimeout(
2046
- () => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),
2047
- remaining()
2048
- );
2049
- })
2050
- ]);
2051
- } finally {
2052
- if (timer) clearTimeout(timer);
2061
+ await page.evaluate(
2062
+ async ({ timeoutMs, expectsInitialAppPaint: expectsInitialAppPaint2 }) => {
2063
+ let deadline = Date.now() + timeoutMs;
2064
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2065
+ const remaining = () => Math.max(1, deadline - Date.now());
2066
+ const withTimeout = async (label, promise) => {
2067
+ let timer = null;
2068
+ try {
2069
+ await Promise.race([
2070
+ promise,
2071
+ new Promise((_, reject) => {
2072
+ timer = setTimeout(
2073
+ () => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),
2074
+ remaining()
2075
+ );
2076
+ })
2077
+ ]);
2078
+ } finally {
2079
+ if (timer) clearTimeout(timer);
2080
+ }
2081
+ };
2082
+ const isTenantOnly = new URL(window.location.href).searchParams.get("renderMode") === "tenant-only";
2083
+ let shellHost = null;
2084
+ while (Date.now() < deadline) {
2085
+ const candidate = window.SootSim?.bridges?.shellHost;
2086
+ if (candidate || isTenantOnly) {
2087
+ shellHost = candidate ?? null;
2088
+ break;
2089
+ }
2090
+ await sleep(25);
2053
2091
  }
2054
- };
2055
- const isTenantOnly = new URL(window.location.href).searchParams.get("renderMode") === "tenant-only";
2056
- let shellHost = null;
2057
- while (Date.now() < deadline) {
2058
- const candidate = window.SootSim?.bridges?.shellHost;
2059
- if (candidate || isTenantOnly) {
2060
- shellHost = candidate ?? null;
2061
- break;
2092
+ if (!shellHost) {
2093
+ if (isTenantOnly) return;
2094
+ throw new Error(`sootsim shell host unavailable after ${timeoutMs}ms`);
2062
2095
  }
2063
- await sleep(25);
2064
- }
2065
- if (!shellHost) {
2066
- if (isTenantOnly) return;
2067
- throw new Error(`sootsim shell host unavailable after ${timeoutMs}ms`);
2068
- }
2069
- if (shellHost.ready && typeof shellHost.ready.then === "function") {
2070
- await withTimeout("sootsim shell ready", shellHost.ready);
2071
- }
2072
- const isRenderHostDisposed = (error) => error instanceof Error && error.message.includes("render host disposed");
2073
- for (; ; ) {
2074
- try {
2075
- if (expectsInitialAppPaint2) {
2076
- while (Date.now() < deadline) {
2077
- if (document.documentElement.dataset.sootsimBootStage === "painted") {
2078
- break;
2079
- }
2080
- await sleep(50);
2081
- }
2082
- if (document.documentElement.dataset.sootsimBootStage !== "painted") {
2083
- throw new Error(
2084
- `sootsim app first content paint timed out after ${timeoutMs}ms`
2085
- );
2086
- }
2087
- } else if (shellHost.firstContentPainted && typeof shellHost.firstContentPainted.then === "function") {
2088
- await withTimeout(
2089
- "sootsim shell first content paint",
2090
- shellHost.firstContentPainted
2091
- );
2096
+ if (shellHost.ready && typeof shellHost.ready.then === "function") {
2097
+ await withTimeout("sootsim shell ready", shellHost.ready);
2098
+ }
2099
+ if (expectsInitialAppPaint2) {
2100
+ const compositorHost = Reflect.get(window, "__sootsimCompositor");
2101
+ if (typeof compositorHost?.firstContentPainted?.then !== "function") {
2102
+ throw new Error("sootsim app compositor unavailable at boot");
2092
2103
  }
2093
- const waitForScreenTransitions = window.__sootsimTest?.waitForScreenTransitions;
2094
- if (typeof waitForScreenTransitions === "function") {
2095
- const transitionResult = await waitForScreenTransitions({
2096
- timeoutMs: Math.min(remaining(), 3e3),
2097
- settleMs: 64,
2098
- startWindowMs: 700
2099
- });
2100
- if (transitionResult?.timedOut === true) {
2101
- throw new Error(
2102
- `sootsim app launch transitions timed out: ${JSON.stringify(transitionResult)}`
2104
+ await compositorHost.firstContentPainted;
2105
+ deadline = Date.now() + 3e3;
2106
+ }
2107
+ const isRenderHostDisposed = (error) => error instanceof Error && error.message.includes("render host disposed");
2108
+ for (; ; ) {
2109
+ try {
2110
+ if (!expectsInitialAppPaint2 && shellHost.firstContentPainted && typeof shellHost.firstContentPainted.then === "function") {
2111
+ await withTimeout(
2112
+ "sootsim shell first content paint",
2113
+ shellHost.firstContentPainted
2103
2114
  );
2104
2115
  }
2116
+ const waitForScreenTransitions = window.__sootsimTest?.waitForScreenTransitions;
2117
+ if (typeof waitForScreenTransitions === "function") {
2118
+ const transitionResult = await waitForScreenTransitions({
2119
+ timeoutMs: Math.min(remaining(), 3e3),
2120
+ settleMs: 64,
2121
+ startWindowMs: 700
2122
+ });
2123
+ if (transitionResult?.timedOut === true) {
2124
+ throw new Error(
2125
+ `sootsim app launch transitions timed out: ${JSON.stringify(transitionResult)}`
2126
+ );
2127
+ }
2128
+ }
2129
+ break;
2130
+ } catch (error) {
2131
+ if (!isRenderHostDisposed(error) || Date.now() >= deadline) throw error;
2132
+ await sleep(100);
2105
2133
  }
2106
- break;
2107
- } catch (error) {
2108
- if (!isRenderHostDisposed(error) || Date.now() >= deadline) throw error;
2109
- await sleep(100);
2110
2134
  }
2111
- }
2112
- }, { timeoutMs: timeout, expectsInitialAppPaint });
2135
+ },
2136
+ { timeoutMs: timeout, expectsInitialAppPaint }
2137
+ );
2113
2138
  } catch (error) {
2114
2139
  throw await describeSootsimBridgeFailure(page, error);
2115
2140
  }
@@ -2418,7 +2443,7 @@ async function dispatchTapAtSootsimPoint(page, x, y) {
2418
2443
  }
2419
2444
  async function isFocusedTextInputNode(page, nodeInfo) {
2420
2445
  const focused = await page.evaluate(
2421
- () => window.__sootsimTest.getFocusedNode?.() ?? null
2446
+ async () => await window.__sootsimTest.getFocusedNode?.() ?? null
2422
2447
  );
2423
2448
  return !!focused && typeof focused === "object" && focused.nodeId === nodeInfo?.nodeId;
2424
2449
  }
@@ -2430,7 +2455,14 @@ async function focusElementForTextEntry(el) {
2430
2455
  }
2431
2456
  if (await isFocusedTextInputNode(page, node)) return;
2432
2457
  await el.tap();
2433
- await page.waitForTimeout(100);
2458
+ await pollUntil(
2459
+ () => page.evaluate(async () => (await window.__sootsimTest.getFocusedNode?.())?.nodeId ?? null),
2460
+ (focusedNodeId) => focusedNodeId === node.nodeId,
2461
+ {
2462
+ timeoutMs: 5e3,
2463
+ label: `SootSim text input ${node.nodeId} to focus`
2464
+ }
2465
+ );
2434
2466
  }
2435
2467
  function ensureScreenshotDir() {
2436
2468
  if (!fs.existsSync(SCREENSHOT_DIR)) {
@@ -2512,9 +2544,9 @@ async function captureSootsimPng(opts) {
2512
2544
  await setSootsimCursorCaptureActive(page, false);
2513
2545
  }
2514
2546
  }
2515
- async function writeSootsimScreenshot(name, opts) {
2547
+ async function writeSootsimScreenshot(name2, opts) {
2516
2548
  ensureScreenshotDir();
2517
- const screenshotPath = path.join(SCREENSHOT_DIR, `${name}.png`);
2549
+ const screenshotPath = path.join(SCREENSHOT_DIR, `${name2}.png`);
2518
2550
  fs.writeFileSync(screenshotPath, await captureSootsimPng(opts));
2519
2551
  return screenshotPath;
2520
2552
  }
@@ -2533,6 +2565,24 @@ async function dispatchStatusBarOverride(config) {
2533
2565
  );
2534
2566
  await page.waitForTimeout(50);
2535
2567
  }
2568
+ async function dispatchDeviceOrientation(orientation) {
2569
+ const page = getPage();
2570
+ await page.evaluate(
2571
+ ({ eventType, next, landscapeSide }) => {
2572
+ window.dispatchEvent(
2573
+ new CustomEvent(eventType, {
2574
+ detail: { type: "set-device-orientation", orientation: next, landscapeSide }
2575
+ })
2576
+ );
2577
+ },
2578
+ {
2579
+ eventType: SOOTSIM_SHELL_COMMAND_NAME,
2580
+ next: orientation,
2581
+ landscapeSide: DETOX_PLATFORM === "ios" ? "right" : "left"
2582
+ }
2583
+ );
2584
+ await page.waitForTimeout(50);
2585
+ }
2536
2586
  var MAX_SETTLED_SPEED_PT_PER_SEC = 2;
2537
2587
  async function waitForFrameClock(page, minMs = 25) {
2538
2588
  const start = performance.now();
@@ -2686,10 +2736,7 @@ function createSootSimElement(matcher) {
2686
2736
  if (typeof longPress !== "function") {
2687
2737
  throw new Error("sootsim longPress bridge is not installed");
2688
2738
  }
2689
- const hit = await longPress(x, y, duration2);
2690
- if (!hit) {
2691
- throw new Error(`sootsim longPress missed at ${x},${y}`);
2692
- }
2739
+ await longPress(x, y, duration2);
2693
2740
  },
2694
2741
  { duration, x: target.x, y: target.y }
2695
2742
  );
@@ -2714,15 +2761,25 @@ function createSootSimElement(matcher) {
2714
2761
  const endSootSimY = tgtNode.absolutePosition.y + tgtNode.layout.height * normalizedEndY;
2715
2762
  const startPage = await sootsimToPage(page, startSootSimX, startSootSimY);
2716
2763
  const endPage = await sootsimToPage(page, endSootSimX, endSootSimY);
2717
- const steps = speed === "slow" ? 20 : 10;
2764
+ const pace = 25 * (speed === "slow" ? 0.1 : 0.5);
2765
+ const steps = Math.max(
2766
+ 1,
2767
+ Math.round(
2768
+ Math.max(
2769
+ Math.abs(endSootSimX - startSootSimX),
2770
+ Math.abs(endSootSimY - startSootSimY)
2771
+ ) / pace
2772
+ )
2773
+ );
2774
+ const frameMs = 1e3 / 60;
2718
2775
  await page.mouse.move(startPage.x, startPage.y);
2719
2776
  await page.mouse.down();
2720
- await page.waitForTimeout(Math.max(duration, holdDuration));
2777
+ await page.waitForTimeout(duration);
2721
2778
  const dx = (endPage.x - startPage.x) / steps;
2722
2779
  const dy = (endPage.y - startPage.y) / steps;
2723
- for (let i = 1; i <= steps; i++) {
2780
+ for (let i = 1; i <= steps + 1; i++) {
2781
+ await page.waitForTimeout(i === steps + 1 ? holdDuration : frameMs);
2724
2782
  await page.mouse.move(startPage.x + dx * i, startPage.y + dy * i);
2725
- await page.waitForTimeout(speed === "slow" ? 30 : 10);
2726
2783
  }
2727
2784
  await page.mouse.up();
2728
2785
  await page.waitForTimeout(50);
@@ -2747,6 +2804,7 @@ function createSootSimElement(matcher) {
2747
2804
  if (replaced === true) return;
2748
2805
  }
2749
2806
  await focusElementForTextEntry(el);
2807
+ await page.keyboard.press("End");
2750
2808
  const value = typeof node?.text === "string" ? node.text : "";
2751
2809
  for (let i = 0; i < value.length; i++) {
2752
2810
  await page.keyboard.press("Backspace");
@@ -2756,6 +2814,7 @@ function createSootSimElement(matcher) {
2756
2814
  async clearText() {
2757
2815
  const page = getPage();
2758
2816
  await focusElementForTextEntry(el);
2817
+ await page.keyboard.press("End");
2759
2818
  const node = await findNodeByMatcher(matcher);
2760
2819
  const value = typeof node?.text === "string" ? node.text : "";
2761
2820
  for (let i = 0; i < value.length; i++) {
@@ -2982,7 +3041,7 @@ function createSootSimElement(matcher) {
2982
3041
  stepDelayMs: speed === "fast" ? 14 : 24
2983
3042
  });
2984
3043
  },
2985
- async takeScreenshot(name) {
3044
+ async takeScreenshot(name2) {
2986
3045
  const node = await findNodeByMatcher(matcher);
2987
3046
  if (!node)
2988
3047
  throw new Error(
@@ -2994,7 +3053,7 @@ function createSootSimElement(matcher) {
2994
3053
  width: node.layout.width,
2995
3054
  height: node.layout.height
2996
3055
  };
2997
- return writeSootsimScreenshot(name, {
3056
+ return writeSootsimScreenshot(name2, {
2998
3057
  crop: {
2999
3058
  x: box.x,
3000
3059
  y: box.y,
@@ -3160,14 +3219,14 @@ var device = {
3160
3219
  await waitForSootsimShellReady(page, 1e4);
3161
3220
  await waitForSootsimWorkletsIdle(page, 3e3);
3162
3221
  },
3163
- async takeScreenshot(name) {
3164
- return writeSootsimScreenshot(name);
3222
+ async takeScreenshot(name2) {
3223
+ return writeSootsimScreenshot(name2);
3165
3224
  },
3166
3225
  // single-shot variant for multi-stage animation captures. skips the
3167
3226
  // frame-stability poll so callers can sample mid-transition.
3168
- async takeScreenshotFast(name) {
3227
+ async takeScreenshotFast(name2) {
3169
3228
  ensureScreenshotDir();
3170
- const screenshotPath = path.join(SCREENSHOT_DIR, `${name}.png`);
3229
+ const screenshotPath = path.join(SCREENSHOT_DIR, `${name2}.png`);
3171
3230
  fs.writeFileSync(screenshotPath, await captureSootsimPngFast());
3172
3231
  return screenshotPath;
3173
3232
  },
@@ -3337,6 +3396,7 @@ var device = {
3337
3396
  snapshot.bitmap.close();
3338
3397
  return {
3339
3398
  actualMs: snapshot.actualMs,
3399
+ anchorId: snapshot.anchorId,
3340
3400
  dataUrl: canvas.toDataURL("image/png"),
3341
3401
  label: snapshot.label,
3342
3402
  offsetMs: snapshot.offsetMs,
@@ -3381,6 +3441,7 @@ var device = {
3381
3441
  holdState.released = true;
3382
3442
  await interact.touchUp(holdState.x, holdState.y);
3383
3443
  };
3444
+ const keptFrames = /* @__PURE__ */ new Map();
3384
3445
  try {
3385
3446
  const baselineBitmap = await captureBitmap();
3386
3447
  const baseline = regionPixels(baselineBitmap);
@@ -3389,7 +3450,9 @@ var device = {
3389
3450
  if (input.hold && interact) {
3390
3451
  const hit = await interact.touchDown(input.hold.x, input.hold.y);
3391
3452
  if (!hit) {
3392
- throw new Error(`peach motion hold missed at ${input.hold.x},${input.hold.y}`);
3453
+ throw new Error(
3454
+ `peach motion hold missed at ${input.hold.x},${input.hold.y}`
3455
+ );
3393
3456
  }
3394
3457
  } else {
3395
3458
  while (!state.triggerCompleted) {
@@ -3399,6 +3462,8 @@ var device = {
3399
3462
  }
3400
3463
  let visibleStart = null;
3401
3464
  let detectedMeasurement = null;
3465
+ let previousReceipt = null;
3466
+ let previousBitmap = null;
3402
3467
  const detectionFrameLimit = Math.ceil(input.detectionTimeoutMs * 60 / 1e3);
3403
3468
  for (let frame = 0; frame < detectionFrameLimit; frame++) {
3404
3469
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
@@ -3414,29 +3479,137 @@ var device = {
3414
3479
  label: "SootSim visible-change detector",
3415
3480
  width: bounds.width
3416
3481
  });
3417
- candidateBitmap.close();
3418
3482
  if (measurement.passedMinimum) {
3483
+ candidateBitmap.close();
3419
3484
  visibleStart = receipt;
3420
3485
  detectedMeasurement = measurement;
3421
3486
  break;
3422
3487
  }
3488
+ previousBitmap?.close();
3489
+ previousBitmap = candidateBitmap;
3490
+ previousReceipt = receipt;
3491
+ }
3492
+ if (previousReceipt && previousBitmap) {
3493
+ keptFrames.set(previousReceipt.frameIndex, previousBitmap);
3423
3494
  }
3424
3495
  if (!visibleStart || !detectedMeasurement) {
3425
3496
  throw new Error(
3426
3497
  `peach motion had no visible change before ${input.detectionTimeoutMs}ms`
3427
3498
  );
3428
3499
  }
3500
+ const driveStart = previousReceipt ?? visibleStart;
3501
+ let currentReceipt = visibleStart;
3502
+ const resolvedAnchors = /* @__PURE__ */ new Map();
3503
+ const anchorRecords = [];
3504
+ const resolveAnchor = async (stage) => {
3505
+ const anchor = stage.anchor;
3506
+ if (!anchor) return { anchorId: "transition", receipt: driveStart };
3507
+ const known = resolvedAnchors.get(anchor.id);
3508
+ if (known) return { anchorId: anchor.id, receipt: known };
3509
+ if (typeof anchor.notBeforeMs !== "number" || !Number.isFinite(anchor.notBeforeMs) || anchor.notBeforeMs < 0) {
3510
+ throw new Error(
3511
+ `peach motion stage ${stage.label} anchor ${anchor.id} needs a non-negative notBeforeMs`
3512
+ );
3513
+ }
3514
+ const onsetThresholdPercent = anchor.onsetThresholdPercent ?? 5;
3515
+ const searchStartVts = driveStart.virtualTimestampMs + anchor.notBeforeMs;
3516
+ if (currentReceipt.virtualTimestampMs > searchStartVts) {
3517
+ throw new Error(
3518
+ `peach motion anchor ${anchor.id} searches from ${anchor.notBeforeMs}ms but the clock already sits past it: the ladder is unexecutable`
3519
+ );
3520
+ }
3521
+ let searchReceipt = currentReceipt;
3522
+ while (searchReceipt.virtualTimestampMs < searchStartVts) {
3523
+ if (state.cancelled) throw new Error("peach motion capture was cancelled");
3524
+ searchReceipt = await controller.stepFrame();
3525
+ await advanceHold(searchReceipt);
3526
+ }
3527
+ currentReceipt = searchReceipt;
3528
+ let anchorBitmap = await captureBitmap();
3529
+ let previousPixels = regionPixels(anchorBitmap);
3530
+ let anchorReceipt = searchReceipt;
3531
+ let onsetMeasurement = null;
3532
+ let panPeakPercent = 0;
3533
+ let latePeakPercent = 0;
3534
+ for (let frame = 0; frame < detectionFrameLimit; frame++) {
3535
+ if (state.cancelled) throw new Error("peach motion capture was cancelled");
3536
+ const receipt = await controller.stepFrame();
3537
+ await advanceHold(receipt);
3538
+ const candidateBitmap = await captureBitmap();
3539
+ const bounds = boundsFor(candidateBitmap);
3540
+ const candidate = regionPixels(candidateBitmap);
3541
+ const measurement = measureChange({
3542
+ baseline: previousPixels,
3543
+ candidate,
3544
+ definition: {
3545
+ ...input.motionChangeDefinition,
3546
+ minimumChangePercent: onsetThresholdPercent
3547
+ },
3548
+ height: bounds.height,
3549
+ label: `SootSim second-drive detector ${anchor.id}`,
3550
+ width: bounds.width
3551
+ });
3552
+ previousPixels = candidate;
3553
+ currentReceipt = receipt;
3554
+ if (receipt.virtualTimestampMs - searchStartVts < 250) {
3555
+ panPeakPercent = Math.max(panPeakPercent, measurement.changePercent);
3556
+ } else {
3557
+ latePeakPercent = Math.max(latePeakPercent, measurement.changePercent);
3558
+ }
3559
+ if (measurement.passedMinimum) {
3560
+ candidateBitmap.close();
3561
+ onsetMeasurement = measurement;
3562
+ break;
3563
+ }
3564
+ anchorBitmap.close();
3565
+ anchorBitmap = candidateBitmap;
3566
+ anchorReceipt = receipt;
3567
+ }
3568
+ if (!onsetMeasurement) {
3569
+ anchorBitmap.close();
3570
+ throw new Error(
3571
+ `peach motion anchor ${anchor.id} found no onset past ${onsetThresholdPercent}% within ${input.detectionTimeoutMs}ms of ${anchor.notBeforeMs}ms (pan peak ${panPeakPercent.toFixed(2)}%, later peak ${latePeakPercent.toFixed(2)}%): the gate or the search start is wrong for this surface`
3572
+ );
3573
+ }
3574
+ keptFrames.get(anchorReceipt.frameIndex)?.close();
3575
+ keptFrames.set(anchorReceipt.frameIndex, anchorBitmap);
3576
+ resolvedAnchors.set(anchor.id, anchorReceipt);
3577
+ anchorRecords.push({
3578
+ changedPixelPercent: onsetMeasurement.changePercent,
3579
+ frameIndex: anchorReceipt.frameIndex,
3580
+ id: anchor.id,
3581
+ notBeforeMs: anchor.notBeforeMs,
3582
+ virtualTimestampMs: anchorReceipt.virtualTimestampMs
3583
+ });
3584
+ return { anchorId: anchor.id, receipt: anchorReceipt };
3585
+ };
3429
3586
  const snapshots = [];
3430
3587
  for (const stage of input.stages) {
3431
3588
  if (state.cancelled) throw new Error("peach motion capture was cancelled");
3432
- const stageFrameIndex = visibleStart.frameIndex + Math.ceil(stage.offsetMs / frameMs);
3589
+ const { anchorId, receipt: anchorReceipt } = await resolveAnchor(stage);
3590
+ const stageFrameIndex = anchorReceipt.frameIndex + Math.ceil(stage.offsetMs / frameMs);
3591
+ const kept = keptFrames.get(stageFrameIndex);
3592
+ if (kept && stageFrameIndex < currentReceipt.frameIndex) {
3593
+ snapshots.push({
3594
+ actualMs: 0,
3595
+ anchorId,
3596
+ bitmap: await createImageBitmap(kept),
3597
+ frameIndex: anchorReceipt.frameIndex,
3598
+ label: stage.label,
3599
+ offsetMs: stage.offsetMs,
3600
+ virtualTimestampMs: anchorReceipt.virtualTimestampMs
3601
+ });
3602
+ continue;
3603
+ }
3433
3604
  for (let eventFrameIndex = nextHoldEventFrameIndex(); eventFrameIndex !== null && stageFrameIndex > eventFrameIndex; eventFrameIndex = nextHoldEventFrameIndex()) {
3434
3605
  await advanceHold(await controller.stepFrame());
3435
3606
  }
3436
- const receipt = await controller.stepToOffset(visibleStart, stage.offsetMs);
3607
+ const receipt = await controller.stepToOffset(anchorReceipt, stage.offsetMs);
3437
3608
  await advanceHold(receipt);
3609
+ currentReceipt = receipt;
3438
3610
  snapshots.push({
3439
- actualMs: receipt.virtualTimestampMs - visibleStart.virtualTimestampMs,
3611
+ actualMs: receipt.virtualTimestampMs - anchorReceipt.virtualTimestampMs,
3612
+ anchorId,
3440
3613
  bitmap: await captureBitmap(),
3441
3614
  frameIndex: receipt.frameIndex,
3442
3615
  label: stage.label,
@@ -3444,14 +3617,28 @@ var device = {
3444
3617
  virtualTimestampMs: receipt.virtualTimestampMs
3445
3618
  });
3446
3619
  }
3620
+ let releasedOffsetMs = 0;
3621
+ while (nextHoldEventFrameIndex() !== null) {
3622
+ const receipt = await controller.stepFrame();
3623
+ await advanceHold(receipt);
3624
+ releasedOffsetMs = receipt.virtualTimestampMs - driveStart.virtualTimestampMs;
3625
+ }
3447
3626
  if (input.settleMs > 0) {
3448
- const maximumOffsetMs = input.stages.reduce(
3449
- (maximum, stage) => Math.max(maximum, stage.offsetMs),
3450
- 0
3627
+ const maximumPrimaryMs = snapshots.reduce(
3628
+ (maximum, snapshot) => Math.max(
3629
+ maximum,
3630
+ snapshot.virtualTimestampMs - driveStart.virtualTimestampMs
3631
+ ),
3632
+ releasedOffsetMs
3451
3633
  );
3452
- await controller.stepToOffset(visibleStart, maximumOffsetMs + input.settleMs);
3634
+ await controller.stepToOffset(driveStart, maximumPrimaryMs + input.settleMs);
3453
3635
  }
3454
3636
  return {
3637
+ anchors: anchorRecords,
3638
+ driveStart: {
3639
+ frameIndex: driveStart.frameIndex,
3640
+ virtualTimestampMs: driveStart.virtualTimestampMs
3641
+ },
3455
3642
  frames: snapshots.map(encode),
3456
3643
  visibleChangeDetection: {
3457
3644
  actualMs: visibleStart.frameIndex * 1e3 / 60,
@@ -3465,6 +3652,7 @@ var device = {
3465
3652
  }
3466
3653
  };
3467
3654
  } finally {
3655
+ for (const bitmap of keptFrames.values()) bitmap.close();
3468
3656
  controller.end();
3469
3657
  delete registry[input.token];
3470
3658
  }
@@ -3506,6 +3694,8 @@ var device = {
3506
3694
  }, token);
3507
3695
  const capture = await capturePromise;
3508
3696
  return {
3697
+ anchors: capture.anchors,
3698
+ driveStart: capture.driveStart,
3509
3699
  frames: writeFastScreenshotFrames(namePrefix, capture.frames),
3510
3700
  triggerCompletedMs,
3511
3701
  triggerStartMs,
@@ -3739,6 +3929,15 @@ var device = {
3739
3929
  async setStatusBar(config) {
3740
3930
  await dispatchStatusBarOverride(config);
3741
3931
  },
3932
+ async setOrientation(orientation) {
3933
+ const next = orientation === "portrait" ? "portrait" : orientation === "landscape" ? "landscape" : null;
3934
+ if (!next) {
3935
+ throw new Error(
3936
+ `sootsim device.setOrientation supports portrait and landscape; got ${orientation}`
3937
+ );
3938
+ }
3939
+ await dispatchDeviceOrientation(next);
3940
+ },
3742
3941
  async setURLBlacklist(urls) {
3743
3942
  },
3744
3943
  async enableSynchronization() {