sootsim 0.1.239 → 0.1.241

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 (146) hide show
  1. package/dist-cli/bin.js +4 -4
  2. package/dist-cli/chunks/{agent-5KVGVFXJ.js → agent-7Z3WG3YJ.js} +2 -2
  3. package/dist-cli/chunks/{agent-wrapper-YVXMSV3X.js → agent-wrapper-IMTGEJ7T.js} +2 -2
  4. package/dist-cli/chunks/{app-fonts-IXF6YPQZ.js → app-fonts-7E66PSAV.js} +2 -2
  5. package/dist-cli/chunks/{assert-FMNM5VTT.js → assert-IEBAQZC7.js} +2 -2
  6. package/dist-cli/chunks/{auth-LNSDYZU4.js → auth-FRMHPA2X.js} +2 -2
  7. package/dist-cli/chunks/beta-AMMCLKHD.js +2 -0
  8. package/dist-cli/chunks/{camera-INMAD3CG.js → camera-O67TQUAH.js} +2 -2
  9. package/dist-cli/chunks/{chunk-WXYRA447.js → chunk-2DY3JHFK.js} +1 -1
  10. package/dist-cli/chunks/{chunk-ZL2KSYJP.js → chunk-4JSCYLLH.js} +1 -1
  11. package/dist-cli/chunks/{chunk-HV2ZRFGP.js → chunk-4V6MFWDY.js} +1 -1
  12. package/dist-cli/chunks/{chunk-BWKVAEAJ.js → chunk-5COOJUTE.js} +1 -1
  13. package/dist-cli/chunks/{chunk-XWPQVHOL.js → chunk-5ZNYFTOB.js} +2 -2
  14. package/dist-cli/chunks/{chunk-QHJLYAOH.js → chunk-7I3PV2RE.js} +2 -2
  15. package/dist-cli/chunks/{chunk-JBUMYCFR.js → chunk-AROVTS3P.js} +2 -2
  16. package/dist-cli/chunks/{chunk-LWE27HF7.js → chunk-BIHEU677.js} +3 -3
  17. package/dist-cli/chunks/{chunk-UZRZGXQ4.js → chunk-CAOW2O5V.js} +2 -2
  18. package/dist-cli/chunks/{chunk-SOZIIFPY.js → chunk-CSZOZRKB.js} +3 -3
  19. package/dist-cli/chunks/{chunk-VIM3WDAA.js → chunk-DS2KFLBO.js} +1 -1
  20. package/dist-cli/chunks/chunk-ECRSTV5C.js +118 -0
  21. package/dist-cli/chunks/{chunk-NJQSCTFC.js → chunk-EY3ZQWUQ.js} +52 -52
  22. package/dist-cli/chunks/{chunk-544G2PCM.js → chunk-F5YD4BLA.js} +2 -2
  23. package/dist-cli/chunks/{chunk-X7JZ4KTU.js → chunk-FR7JCFAR.js} +1 -1
  24. package/dist-cli/chunks/{chunk-YIS2RXPE.js → chunk-GFZLDVLE.js} +1 -1
  25. package/dist-cli/chunks/{chunk-PVIUYYIS.js → chunk-GVTSKLLS.js} +2 -2
  26. package/dist-cli/chunks/{chunk-KCS6OYQV.js → chunk-H7G2QGUG.js} +2 -2
  27. package/dist-cli/chunks/{chunk-NDU5EJFW.js → chunk-H7SP6J3H.js} +2 -2
  28. package/dist-cli/chunks/{chunk-CJRALJVV.js → chunk-HNI6U6M5.js} +1 -1
  29. package/dist-cli/chunks/{chunk-P5JIE7R6.js → chunk-IF5VXL45.js} +2 -2
  30. package/dist-cli/chunks/{chunk-BKDHODNB.js → chunk-JMTQBHQY.js} +1 -1
  31. package/dist-cli/chunks/{chunk-CUDTQFGP.js → chunk-JX4N376W.js} +2 -2
  32. package/dist-cli/chunks/{chunk-FOTAUF7Q.js → chunk-KFI26HW5.js} +1 -1
  33. package/dist-cli/chunks/chunk-KLXLOMI6.js +1 -0
  34. package/dist-cli/chunks/{chunk-JPNTLGLA.js → chunk-KN3Q6VSC.js} +2 -2
  35. package/dist-cli/chunks/{chunk-JOSDTQZ3.js → chunk-LVUOVKJO.js} +2 -2
  36. package/dist-cli/chunks/{chunk-VBO7OQLR.js → chunk-MCAJBAWD.js} +2 -2
  37. package/dist-cli/chunks/{chunk-R5I2OBQM.js → chunk-MGLTIS7P.js} +2 -2
  38. package/dist-cli/chunks/{chunk-SLIYVKRX.js → chunk-PR6AOTP5.js} +1 -1
  39. package/dist-cli/chunks/{chunk-5VCOEPSJ.js → chunk-PVUUG5YW.js} +2 -2
  40. package/dist-cli/chunks/{chunk-EQYRXSBE.js → chunk-R3EZVIEM.js} +2 -2
  41. package/dist-cli/chunks/{chunk-HE5NBBLH.js → chunk-U7XJNNGI.js} +3 -3
  42. package/dist-cli/chunks/{chunk-ECPEYFHV.js → chunk-UF333CGY.js} +2 -2
  43. package/dist-cli/chunks/{chunk-QYVGMN3I.js → chunk-VVUANJFA.js} +72 -23
  44. package/dist-cli/chunks/{chunk-SBSL3DUQ.js → chunk-WFB3ICCN.js} +1 -1
  45. package/dist-cli/chunks/{chunk-6XF2EHL2.js → chunk-WIHKSZM4.js} +1 -1
  46. package/dist-cli/chunks/{chunk-MDOTB5DP.js → chunk-WPWLJFKF.js} +2 -2
  47. package/dist-cli/chunks/{chunk-GI2S43VF.js → chunk-YLWUYESW.js} +1 -1
  48. package/dist-cli/chunks/{chunk-D4YH5KNQ.js → chunk-YX7S6N42.js} +2 -2
  49. package/dist-cli/chunks/{chunk-44WCYVFQ.js → chunk-Z72IF5FC.js} +2 -2
  50. package/dist-cli/chunks/chunk-ZGSGMGE3.js +1 -0
  51. package/dist-cli/chunks/{chunk-SRMYUHBI.js → chunk-ZUZLQ4ZJ.js} +1 -1
  52. package/dist-cli/chunks/{chunk-DJIWF2SR.js → chunk-ZYJCVJFP.js} +1 -1
  53. package/dist-cli/chunks/cli-version-DIYZFJMN.js +2 -0
  54. package/dist-cli/chunks/{compat-VYOIJUW6.js → compat-ZRUVBIK7.js} +3 -3
  55. package/dist-cli/chunks/{config-ZJIIQF6K.js → config-VKQDFC5Z.js} +2 -2
  56. package/dist-cli/chunks/{control-KFAL3LD7.js → control-OYK2J4JW.js} +2 -2
  57. package/dist-cli/chunks/daemon-YQLY7PVM.js +2 -0
  58. package/dist-cli/chunks/{debug-RCY6BSLW.js → debug-3WEO7ZRW.js} +3 -3
  59. package/dist-cli/chunks/{desktop-FRZILY3U.js → desktop-DVOFHJKU.js} +3 -3
  60. package/dist-cli/chunks/{detox-W2HOZRDP.js → detox-GAGC276D.js} +2 -2
  61. package/dist-cli/chunks/{device-WRWT7SEE.js → device-256KFP2G.js} +2 -2
  62. package/dist-cli/chunks/{diagnose-3KSX2NJC.js → diagnose-GYKUU3N5.js} +2 -2
  63. package/dist-cli/chunks/drivers-FHWFHVQF.js +2 -0
  64. package/dist-cli/chunks/{film-QN7NUW6Z.js → film-WAV3WPQK.js} +3 -3
  65. package/dist-cli/chunks/flow-WKPPIDM6.js +2 -0
  66. package/dist-cli/chunks/help-P4A7NNSC.js +2 -0
  67. package/dist-cli/chunks/{hidden-runtime-alias-NIH62HCH.js → hidden-runtime-alias-DODMMNOJ.js} +2 -2
  68. package/dist-cli/chunks/{home-paths-3CPTCZDU.js → home-paths-QU5UX3MT.js} +2 -2
  69. package/dist-cli/chunks/inspect-NIHY67NU.js +2 -0
  70. package/dist-cli/chunks/install-desktop-NA53KD6G.js +2 -0
  71. package/dist-cli/chunks/{login-HQ3356B6.js → login-OURQ3DVM.js} +4 -4
  72. package/dist-cli/chunks/{logout-LVVXQS5S.js → logout-ZYVJAPBO.js} +2 -2
  73. package/dist-cli/chunks/{maestro-34Y7GOG5.js → maestro-H2NGXNF4.js} +3 -3
  74. package/dist-cli/chunks/{maestro-generate-XBIZ7Z6U.js → maestro-generate-DA2IZPK2.js} +3 -3
  75. package/dist-cli/chunks/{mode-VXZR6I46.js → mode-QTK3ERUW.js} +2 -2
  76. package/dist-cli/chunks/optional-demo-registry-ZLN23OYV.js +2 -0
  77. package/dist-cli/chunks/{perf-IBHKDDFN.js → perf-WZ4OPYXV.js} +2 -2
  78. package/dist-cli/chunks/{record-S5WYHPNR.js → record-ZQSUHQJU.js} +3 -3
  79. package/dist-cli/chunks/runtime-RM67M6H3.js +2 -0
  80. package/dist-cli/chunks/{screenshot-command-T65M2SYC.js → screenshot-command-UDJ6TEAM.js} +3 -3
  81. package/dist-cli/chunks/{screenshots-capture-75AHIYWA.js → screenshots-capture-4FK4NYXG.js} +2 -2
  82. package/dist-cli/chunks/serve-MCT4HKR7.js +41 -0
  83. package/dist-cli/chunks/{setup-TLQ2VM3F.js → setup-I2IELNJN.js} +3 -3
  84. package/dist-cli/chunks/{skills-LZZDB6V5.js → skills-PVXI7LM6.js} +2 -2
  85. package/dist-cli/chunks/{storage-G2IZ2WAD.js → storage-KZFCVRGD.js} +2 -2
  86. package/dist-cli/chunks/store-CEMC5MTK.js +2 -0
  87. package/dist-cli/chunks/telemetry-LFIQWPZD.js +2 -0
  88. package/dist-cli/chunks/{timeline-L4CFSXX3.js → timeline-YAULXFEC.js} +2 -2
  89. package/dist-cli/chunks/{upgrade-5USALUAU.js → upgrade-63TVREBM.js} +2 -2
  90. package/dist-cli/chunks/upload-APW46WK3.js +2 -0
  91. package/dist-cli/chunks/{version-RQZPUNJO.js → version-JWWEGZS7.js} +2 -2
  92. package/dist-cli/chunks/{web-YQ6XAB5F.js → web-XO724HJL.js} +2 -2
  93. package/dist-cli/chunks/{what-happened-JLAM7TBD.js → what-happened-DQ4MXAPJ.js} +2 -2
  94. package/dist-lib/agent-daemon-client.cjs +1 -1
  95. package/dist-lib/agent-events.cjs +1 -1
  96. package/dist-lib/agent-identity.cjs +1 -1
  97. package/dist-lib/agent-sessions.cjs +1 -1
  98. package/dist-lib/attached-projects.cjs +1 -1
  99. package/dist-lib/auth/shared-session.cjs +1 -1
  100. package/dist-lib/backend-origin.cjs +1 -1
  101. package/dist-lib/beta.cjs +1 -1
  102. package/dist-lib/beta.mjs +1 -1
  103. package/dist-lib/bridge-constants.cjs +1 -1
  104. package/dist-lib/cli-constants.cjs +1 -1
  105. package/dist-lib/config.cjs +1 -1
  106. package/dist-lib/detox/index.cjs +1 -1
  107. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  108. package/dist-lib/home-paths.cjs +1 -1
  109. package/dist-lib/host/bridge-host.cjs +77 -33
  110. package/dist-lib/host/fetch-proxy-handler.cjs +77 -33
  111. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  112. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  113. package/dist-lib/host/websocket-proxy.cjs +1 -1
  114. package/dist-lib/index.cjs +1 -1
  115. package/dist-lib/jump-to-source-babel.cjs +1 -1
  116. package/dist-lib/menu.cjs +1 -1
  117. package/dist-lib/menu.mjs +1 -1
  118. package/dist-lib/metro.cjs +1 -1
  119. package/dist-lib/profiles.cjs +1 -1
  120. package/dist-lib/render-mode.cjs +1 -1
  121. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  122. package/dist-lib/sdk.cjs +157 -5
  123. package/dist-lib/sdk.mjs +156 -5
  124. package/dist-lib/skills.cjs +221 -37
  125. package/dist-lib/vite.cjs +1 -1
  126. package/package.json +1 -1
  127. package/skills/contrast/SKILL.md +1 -1
  128. package/skills/sootsim-debug/SKILL.md +21 -12
  129. package/src/host/fetch-proxy-handler.ts +104 -52
  130. package/dist-cli/chunks/beta-BH6RPXQU.js +0 -2
  131. package/dist-cli/chunks/chunk-HJW6SASW.js +0 -118
  132. package/dist-cli/chunks/chunk-J7SEKMNI.js +0 -1
  133. package/dist-cli/chunks/chunk-NO6RQXYN.js +0 -1
  134. package/dist-cli/chunks/cli-version-7KCROGJR.js +0 -2
  135. package/dist-cli/chunks/daemon-TJXAVVUE.js +0 -2
  136. package/dist-cli/chunks/drivers-SJOWPOA3.js +0 -2
  137. package/dist-cli/chunks/flow-Q4XCAPIT.js +0 -2
  138. package/dist-cli/chunks/help-FRVJH7SB.js +0 -2
  139. package/dist-cli/chunks/inspect-5ZZN4UJN.js +0 -2
  140. package/dist-cli/chunks/install-desktop-MIY6DLN6.js +0 -2
  141. package/dist-cli/chunks/optional-demo-registry-5JSIUIMF.js +0 -2
  142. package/dist-cli/chunks/runtime-5K5JFYOZ.js +0 -2
  143. package/dist-cli/chunks/serve-OJVA6PNV.js +0 -41
  144. package/dist-cli/chunks/store-X6HDJ5SE.js +0 -2
  145. package/dist-cli/chunks/telemetry-AHMOJS6L.js +0 -2
  146. package/dist-cli/chunks/upload-OWMPT3Z7.js +0 -2
package/dist-lib/sdk.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! sootsim v0.1.239 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! sootsim v0.1.241 | (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 __defProp = Object.defineProperty;
@@ -76,6 +76,7 @@ __export(sdk_exports, {
76
76
  resolveFindMode: () => resolveFindMode,
77
77
  resolveMaxMsFlag: () => resolveMaxMsFlag,
78
78
  scoreInteractive: () => scoreInteractive,
79
+ scoreLayoutTextContrast: () => scoreLayoutTextContrast,
79
80
  setDebugChannels: () => setDebugChannels,
80
81
  shouldSkipAutoSettleForInspectPick: () => shouldSkipAutoSettleForInspectPick,
81
82
  tapBest: () => tapBest,
@@ -236,6 +237,101 @@ async function inspectDescribe(bridge, dumpOpts) {
236
237
  const result = await bridge.send({ type: "evaluate", code });
237
238
  return result ?? {};
238
239
  }
240
+ function scoreLayoutTextContrast(input) {
241
+ const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
242
+ const { normal, background } = input;
243
+ if (normal.width <= 0 || normal.height <= 0 || normal.width !== background.width || normal.height !== background.height) {
244
+ return { ratio: null, required, reason: "capture dimensions do not match" };
245
+ }
246
+ const left = Math.max(0, Math.floor(input.box.x));
247
+ const top = Math.max(0, Math.floor(input.box.y));
248
+ const right = Math.min(normal.width, Math.ceil(input.box.x + input.box.w));
249
+ const bottom = Math.min(normal.height, Math.ceil(input.box.y + input.box.h));
250
+ if (right <= left || bottom <= top) {
251
+ return { ratio: null, required, reason: "text is outside the captured frame" };
252
+ }
253
+ const linear = (value) => {
254
+ const channel = value / 255;
255
+ return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
256
+ };
257
+ const ratioOf = (foreground, behind) => {
258
+ const foregroundLuminance = 0.2126 * linear(foreground[0]) + 0.7152 * linear(foreground[1]) + 0.0722 * linear(foreground[2]);
259
+ const backgroundLuminance = 0.2126 * linear(behind[0]) + 0.7152 * linear(behind[1]) + 0.0722 * linear(behind[2]);
260
+ return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
261
+ };
262
+ const hex = (color) => `#${color.slice(0, 3).map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`;
263
+ const area = (right - left) * (bottom - top);
264
+ const step = Math.max(1, Math.floor(Math.sqrt(area / 12e3)));
265
+ const offsetAt = (x, y) => (y * normal.width + x) * 4;
266
+ const deltaAt = (offset) => Math.max(
267
+ Math.abs(normal.data[offset] - background.data[offset]),
268
+ Math.abs(normal.data[offset + 1] - background.data[offset + 1]),
269
+ Math.abs(normal.data[offset + 2] - background.data[offset + 2])
270
+ );
271
+ let strongestDelta = 0;
272
+ let strongestForeground = null;
273
+ let opaqueBackground = null;
274
+ for (let y = top; y < bottom; y += step) {
275
+ for (let x = left; x < right; x += step) {
276
+ const offset = offsetAt(x, y);
277
+ if (background.data[offset + 3] < 250) continue;
278
+ const behind = [
279
+ background.data[offset],
280
+ background.data[offset + 1],
281
+ background.data[offset + 2]
282
+ ];
283
+ opaqueBackground ??= behind;
284
+ const delta = deltaAt(offset);
285
+ if (delta <= strongestDelta) continue;
286
+ strongestDelta = delta;
287
+ strongestForeground = [
288
+ normal.data[offset],
289
+ normal.data[offset + 1],
290
+ normal.data[offset + 2]
291
+ ];
292
+ }
293
+ }
294
+ let bestRatio = Infinity;
295
+ let bestForeground = null;
296
+ let bestBackground = null;
297
+ const consider = (foreground, behind) => {
298
+ const ratio = ratioOf(foreground, behind);
299
+ if (ratio >= bestRatio) return;
300
+ bestRatio = ratio;
301
+ bestForeground = foreground;
302
+ bestBackground = behind;
303
+ };
304
+ if (strongestDelta >= 2 && strongestForeground) {
305
+ for (let y = top; y < bottom; y += step) {
306
+ for (let x = left; x < right; x += step) {
307
+ const offset = offsetAt(x, y);
308
+ if (background.data[offset + 3] < 250 || deltaAt(offset) < 2) {
309
+ continue;
310
+ }
311
+ consider(strongestForeground, [
312
+ background.data[offset],
313
+ background.data[offset + 1],
314
+ background.data[offset + 2]
315
+ ]);
316
+ }
317
+ }
318
+ } else if (opaqueBackground) {
319
+ consider(opaqueBackground, opaqueBackground);
320
+ }
321
+ if (!bestForeground || !bestBackground || !Number.isFinite(bestRatio)) {
322
+ return {
323
+ ratio: null,
324
+ required,
325
+ reason: "could not isolate opaque rendered text pixels"
326
+ };
327
+ }
328
+ return {
329
+ ratio: Math.round(bestRatio * 100) / 100,
330
+ required,
331
+ foreground: hex(bestForeground),
332
+ background: hex(bestBackground)
333
+ };
334
+ }
239
335
  function layoutGetNativeEval(opts = {}) {
240
336
  const includeStyle = opts.styling === true;
241
337
  const filter = opts.filter?.trim().toLowerCase() ?? "";
@@ -245,6 +341,7 @@ function layoutGetNativeEval(opts = {}) {
245
341
  const list = await t.listInspectable({})
246
342
  if (!Array.isArray(list)) return []
247
343
  const includeStyle = ${JSON.stringify(includeStyle)}
344
+ const scoreContrast = ${scoreLayoutTextContrast.toString()}
248
345
  // narrows by ELEMENT, not by style field: fewer rows, full detail on each.
249
346
  const filter = ${JSON.stringify(filter)}
250
347
  const matchesFilter = (n, text) => {
@@ -289,7 +386,7 @@ function layoutGetNativeEval(opts = {}) {
289
386
  }
290
387
  const weightOf = (style) => {
291
388
  const w = style && style.fontWeight
292
- const n = typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN)
389
+ const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
293
390
  return Number.isFinite(n) && n >= 600 ? n : undefined
294
391
  }
295
392
  const decoOf = (style) => {
@@ -332,7 +429,7 @@ function layoutGetNativeEval(opts = {}) {
332
429
  return abs.x + layout.width > 0 && abs.y + layout.height > 0 && abs.x < screenW && abs.y < screenH
333
430
  }
334
431
 
335
- return list.filter(isVisible).filter((n) => {
432
+ const elements = list.filter(isVisible).filter((n) => {
336
433
  const t = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
337
434
  return matchesFilter(n, t)
338
435
  }).map((n) => {
@@ -367,7 +464,7 @@ function layoutGetNativeEval(opts = {}) {
367
464
  ...base,
368
465
  borderRadius: radiusOf(style),
369
466
  padding: { t: round(pad.top), r: round(pad.right), b: round(pad.bottom), l: round(pad.left) },
370
- fontSize: fontSizeOf(style),
467
+ fontSize: text ? (fontSizeOf(style) || 14) : 0,
371
468
  color: text ? colorStr(style.color) : undefined,
372
469
  bg: colorStr(style.backgroundColor),
373
470
  opacity: opacityOf(style),
@@ -378,6 +475,50 @@ function layoutGetNativeEval(opts = {}) {
378
475
  borderColor: borderWidth ? borderColorOf(style) : undefined,
379
476
  }
380
477
  })
478
+ const textElements = elements.filter((element) => element.text && element.fontSize > 0)
479
+ if (!includeStyle || textElements.length === 0) return elements
480
+
481
+ const screenshot = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.screenshot
482
+ if (typeof screenshot !== 'function') {
483
+ throw new Error('styled native layout requires the SootSim capture bridge')
484
+ }
485
+ const device = list.find((node) => node && node.device)?.device || {}
486
+ const width = Math.round(Number(device.width) || window.innerWidth || 0)
487
+ const height = Math.round(Number(device.height) || window.innerHeight || 0)
488
+ if (!width || !height) throw new Error('native layout has no capture dimensions')
489
+ const capture = async (suppressText) => {
490
+ const dataUrl = await screenshot({
491
+ format: 'png',
492
+ outputWidth: width,
493
+ outputHeight: height,
494
+ ...(suppressText ? { suppressText: true } : {}),
495
+ })
496
+ const image = await new Promise((resolve, reject) => {
497
+ const next = new Image()
498
+ next.onload = () => resolve(next)
499
+ next.onerror = () => reject(new Error('could not decode native layout pixels'))
500
+ next.src = dataUrl
501
+ })
502
+ const canvas = document.createElement('canvas')
503
+ canvas.width = image.naturalWidth
504
+ canvas.height = image.naturalHeight
505
+ const context = canvas.getContext('2d', { willReadFrequently: true })
506
+ if (!context) throw new Error('could not read native layout pixels')
507
+ context.drawImage(image, 0, 0)
508
+ return context.getImageData(0, 0, canvas.width, canvas.height)
509
+ }
510
+ const normal = await capture(false)
511
+ const background = await capture(true)
512
+ for (const element of textElements) {
513
+ element.contrast = scoreContrast({
514
+ normal,
515
+ background,
516
+ box: element.box,
517
+ fontSize: element.fontSize,
518
+ fontWeight: element.fontWeight,
519
+ })
520
+ }
521
+ return elements
381
522
  })()`;
382
523
  }
383
524
  var LAYOUT_GET_NATIVE_EVAL = layoutGetNativeEval();
@@ -404,6 +545,11 @@ function formatLayoutElements(elements, opts) {
404
545
  if (el.fontWeight) parts.push(`weight:${el.fontWeight}`);
405
546
  if (el.color) parts.push(`color:${el.color}`);
406
547
  if (el.bg) parts.push(`bg:${el.bg}`);
548
+ if (el.contrast) {
549
+ const score = el.contrast.ratio == null ? `?/${el.contrast.required}` : `${el.contrast.ratio}/${el.contrast.required}${el.contrast.ratio < el.contrast.required ? " FAIL" : ""}`;
550
+ parts.push(`contrast:${score}`);
551
+ if (el.contrast.background) parts.push(`on:${el.contrast.background}`);
552
+ }
407
553
  if (el.opacity != null) parts.push(`opacity:${el.opacity}`);
408
554
  if (el.textDecoration) parts.push(`deco:${el.textDecoration}`);
409
555
  if (el.overflow) parts.push(`overflow:${el.overflow}`);
@@ -1204,6 +1350,10 @@ async function inspectScreens(bridge) {
1204
1350
  test && typeof test.getNavigationSnapshot === 'function'
1205
1351
  ? await test.getNavigationSnapshot()
1206
1352
  : null
1353
+ const route =
1354
+ test && typeof test.getRouteInfo === 'function'
1355
+ ? await test.getRouteInfo()
1356
+ : null
1207
1357
  const keyboard =
1208
1358
  kb && typeof kb.getLayout === 'function'
1209
1359
  ? (() => {
@@ -1220,13 +1370,14 @@ async function inspectScreens(bridge) {
1220
1370
  } : null
1221
1371
  })()
1222
1372
  : null
1223
- return { nav: navSnap, keyboard }
1373
+ return { nav: navSnap, route, keyboard }
1224
1374
  })()`
1225
1375
  });
1226
1376
  const shell = await getShellState(bridge, 500).catch(() => null);
1227
1377
  return {
1228
1378
  shell,
1229
1379
  nav: payload?.nav ?? null,
1380
+ route: payload?.route ?? null,
1230
1381
  keyboard: payload?.keyboard ?? null
1231
1382
  };
1232
1383
  }
@@ -2084,6 +2235,7 @@ async function tapBest(bridge, query, opts = {}) {
2084
2235
  resolveFindMode,
2085
2236
  resolveMaxMsFlag,
2086
2237
  scoreInteractive,
2238
+ scoreLayoutTextContrast,
2087
2239
  setDebugChannels,
2088
2240
  shouldSkipAutoSettleForInspectPick,
2089
2241
  tapBest,
package/dist-lib/sdk.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! sootsim v0.1.239 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! sootsim v0.1.241 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // cli/commands/inspect/core.ts
4
4
  function resolveMaxMsFlag(args, fallbackMs) {
@@ -146,6 +146,101 @@ async function inspectDescribe(bridge, dumpOpts) {
146
146
  const result = await bridge.send({ type: "evaluate", code });
147
147
  return result ?? {};
148
148
  }
149
+ function scoreLayoutTextContrast(input) {
150
+ const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
151
+ const { normal, background } = input;
152
+ if (normal.width <= 0 || normal.height <= 0 || normal.width !== background.width || normal.height !== background.height) {
153
+ return { ratio: null, required, reason: "capture dimensions do not match" };
154
+ }
155
+ const left = Math.max(0, Math.floor(input.box.x));
156
+ const top = Math.max(0, Math.floor(input.box.y));
157
+ const right = Math.min(normal.width, Math.ceil(input.box.x + input.box.w));
158
+ const bottom = Math.min(normal.height, Math.ceil(input.box.y + input.box.h));
159
+ if (right <= left || bottom <= top) {
160
+ return { ratio: null, required, reason: "text is outside the captured frame" };
161
+ }
162
+ const linear = (value) => {
163
+ const channel = value / 255;
164
+ return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
165
+ };
166
+ const ratioOf = (foreground, behind) => {
167
+ const foregroundLuminance = 0.2126 * linear(foreground[0]) + 0.7152 * linear(foreground[1]) + 0.0722 * linear(foreground[2]);
168
+ const backgroundLuminance = 0.2126 * linear(behind[0]) + 0.7152 * linear(behind[1]) + 0.0722 * linear(behind[2]);
169
+ return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
170
+ };
171
+ const hex = (color) => `#${color.slice(0, 3).map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`;
172
+ const area = (right - left) * (bottom - top);
173
+ const step = Math.max(1, Math.floor(Math.sqrt(area / 12e3)));
174
+ const offsetAt = (x, y) => (y * normal.width + x) * 4;
175
+ const deltaAt = (offset) => Math.max(
176
+ Math.abs(normal.data[offset] - background.data[offset]),
177
+ Math.abs(normal.data[offset + 1] - background.data[offset + 1]),
178
+ Math.abs(normal.data[offset + 2] - background.data[offset + 2])
179
+ );
180
+ let strongestDelta = 0;
181
+ let strongestForeground = null;
182
+ let opaqueBackground = null;
183
+ for (let y = top; y < bottom; y += step) {
184
+ for (let x = left; x < right; x += step) {
185
+ const offset = offsetAt(x, y);
186
+ if (background.data[offset + 3] < 250) continue;
187
+ const behind = [
188
+ background.data[offset],
189
+ background.data[offset + 1],
190
+ background.data[offset + 2]
191
+ ];
192
+ opaqueBackground ?? (opaqueBackground = behind);
193
+ const delta = deltaAt(offset);
194
+ if (delta <= strongestDelta) continue;
195
+ strongestDelta = delta;
196
+ strongestForeground = [
197
+ normal.data[offset],
198
+ normal.data[offset + 1],
199
+ normal.data[offset + 2]
200
+ ];
201
+ }
202
+ }
203
+ let bestRatio = Infinity;
204
+ let bestForeground = null;
205
+ let bestBackground = null;
206
+ const consider = (foreground, behind) => {
207
+ const ratio = ratioOf(foreground, behind);
208
+ if (ratio >= bestRatio) return;
209
+ bestRatio = ratio;
210
+ bestForeground = foreground;
211
+ bestBackground = behind;
212
+ };
213
+ if (strongestDelta >= 2 && strongestForeground) {
214
+ for (let y = top; y < bottom; y += step) {
215
+ for (let x = left; x < right; x += step) {
216
+ const offset = offsetAt(x, y);
217
+ if (background.data[offset + 3] < 250 || deltaAt(offset) < 2) {
218
+ continue;
219
+ }
220
+ consider(strongestForeground, [
221
+ background.data[offset],
222
+ background.data[offset + 1],
223
+ background.data[offset + 2]
224
+ ]);
225
+ }
226
+ }
227
+ } else if (opaqueBackground) {
228
+ consider(opaqueBackground, opaqueBackground);
229
+ }
230
+ if (!bestForeground || !bestBackground || !Number.isFinite(bestRatio)) {
231
+ return {
232
+ ratio: null,
233
+ required,
234
+ reason: "could not isolate opaque rendered text pixels"
235
+ };
236
+ }
237
+ return {
238
+ ratio: Math.round(bestRatio * 100) / 100,
239
+ required,
240
+ foreground: hex(bestForeground),
241
+ background: hex(bestBackground)
242
+ };
243
+ }
149
244
  function layoutGetNativeEval(opts = {}) {
150
245
  const includeStyle = opts.styling === true;
151
246
  const filter = opts.filter?.trim().toLowerCase() ?? "";
@@ -155,6 +250,7 @@ function layoutGetNativeEval(opts = {}) {
155
250
  const list = await t.listInspectable({})
156
251
  if (!Array.isArray(list)) return []
157
252
  const includeStyle = ${JSON.stringify(includeStyle)}
253
+ const scoreContrast = ${scoreLayoutTextContrast.toString()}
158
254
  // narrows by ELEMENT, not by style field: fewer rows, full detail on each.
159
255
  const filter = ${JSON.stringify(filter)}
160
256
  const matchesFilter = (n, text) => {
@@ -199,7 +295,7 @@ function layoutGetNativeEval(opts = {}) {
199
295
  }
200
296
  const weightOf = (style) => {
201
297
  const w = style && style.fontWeight
202
- const n = typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN)
298
+ const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
203
299
  return Number.isFinite(n) && n >= 600 ? n : undefined
204
300
  }
205
301
  const decoOf = (style) => {
@@ -242,7 +338,7 @@ function layoutGetNativeEval(opts = {}) {
242
338
  return abs.x + layout.width > 0 && abs.y + layout.height > 0 && abs.x < screenW && abs.y < screenH
243
339
  }
244
340
 
245
- return list.filter(isVisible).filter((n) => {
341
+ const elements = list.filter(isVisible).filter((n) => {
246
342
  const t = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
247
343
  return matchesFilter(n, t)
248
344
  }).map((n) => {
@@ -277,7 +373,7 @@ function layoutGetNativeEval(opts = {}) {
277
373
  ...base,
278
374
  borderRadius: radiusOf(style),
279
375
  padding: { t: round(pad.top), r: round(pad.right), b: round(pad.bottom), l: round(pad.left) },
280
- fontSize: fontSizeOf(style),
376
+ fontSize: text ? (fontSizeOf(style) || 14) : 0,
281
377
  color: text ? colorStr(style.color) : undefined,
282
378
  bg: colorStr(style.backgroundColor),
283
379
  opacity: opacityOf(style),
@@ -288,6 +384,50 @@ function layoutGetNativeEval(opts = {}) {
288
384
  borderColor: borderWidth ? borderColorOf(style) : undefined,
289
385
  }
290
386
  })
387
+ const textElements = elements.filter((element) => element.text && element.fontSize > 0)
388
+ if (!includeStyle || textElements.length === 0) return elements
389
+
390
+ const screenshot = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.screenshot
391
+ if (typeof screenshot !== 'function') {
392
+ throw new Error('styled native layout requires the SootSim capture bridge')
393
+ }
394
+ const device = list.find((node) => node && node.device)?.device || {}
395
+ const width = Math.round(Number(device.width) || window.innerWidth || 0)
396
+ const height = Math.round(Number(device.height) || window.innerHeight || 0)
397
+ if (!width || !height) throw new Error('native layout has no capture dimensions')
398
+ const capture = async (suppressText) => {
399
+ const dataUrl = await screenshot({
400
+ format: 'png',
401
+ outputWidth: width,
402
+ outputHeight: height,
403
+ ...(suppressText ? { suppressText: true } : {}),
404
+ })
405
+ const image = await new Promise((resolve, reject) => {
406
+ const next = new Image()
407
+ next.onload = () => resolve(next)
408
+ next.onerror = () => reject(new Error('could not decode native layout pixels'))
409
+ next.src = dataUrl
410
+ })
411
+ const canvas = document.createElement('canvas')
412
+ canvas.width = image.naturalWidth
413
+ canvas.height = image.naturalHeight
414
+ const context = canvas.getContext('2d', { willReadFrequently: true })
415
+ if (!context) throw new Error('could not read native layout pixels')
416
+ context.drawImage(image, 0, 0)
417
+ return context.getImageData(0, 0, canvas.width, canvas.height)
418
+ }
419
+ const normal = await capture(false)
420
+ const background = await capture(true)
421
+ for (const element of textElements) {
422
+ element.contrast = scoreContrast({
423
+ normal,
424
+ background,
425
+ box: element.box,
426
+ fontSize: element.fontSize,
427
+ fontWeight: element.fontWeight,
428
+ })
429
+ }
430
+ return elements
291
431
  })()`;
292
432
  }
293
433
  var LAYOUT_GET_NATIVE_EVAL = layoutGetNativeEval();
@@ -314,6 +454,11 @@ function formatLayoutElements(elements, opts) {
314
454
  if (el.fontWeight) parts.push(`weight:${el.fontWeight}`);
315
455
  if (el.color) parts.push(`color:${el.color}`);
316
456
  if (el.bg) parts.push(`bg:${el.bg}`);
457
+ if (el.contrast) {
458
+ const score = el.contrast.ratio == null ? `?/${el.contrast.required}` : `${el.contrast.ratio}/${el.contrast.required}${el.contrast.ratio < el.contrast.required ? " FAIL" : ""}`;
459
+ parts.push(`contrast:${score}`);
460
+ if (el.contrast.background) parts.push(`on:${el.contrast.background}`);
461
+ }
317
462
  if (el.opacity != null) parts.push(`opacity:${el.opacity}`);
318
463
  if (el.textDecoration) parts.push(`deco:${el.textDecoration}`);
319
464
  if (el.overflow) parts.push(`overflow:${el.overflow}`);
@@ -1114,6 +1259,10 @@ async function inspectScreens(bridge) {
1114
1259
  test && typeof test.getNavigationSnapshot === 'function'
1115
1260
  ? await test.getNavigationSnapshot()
1116
1261
  : null
1262
+ const route =
1263
+ test && typeof test.getRouteInfo === 'function'
1264
+ ? await test.getRouteInfo()
1265
+ : null
1117
1266
  const keyboard =
1118
1267
  kb && typeof kb.getLayout === 'function'
1119
1268
  ? (() => {
@@ -1130,13 +1279,14 @@ async function inspectScreens(bridge) {
1130
1279
  } : null
1131
1280
  })()
1132
1281
  : null
1133
- return { nav: navSnap, keyboard }
1282
+ return { nav: navSnap, route, keyboard }
1134
1283
  })()`
1135
1284
  });
1136
1285
  const shell = await getShellState(bridge, 500).catch(() => null);
1137
1286
  return {
1138
1287
  shell,
1139
1288
  nav: payload?.nav ?? null,
1289
+ route: payload?.route ?? null,
1140
1290
  keyboard: payload?.keyboard ?? null
1141
1291
  };
1142
1292
  }
@@ -1993,6 +2143,7 @@ export {
1993
2143
  resolveFindMode,
1994
2144
  resolveMaxMsFlag,
1995
2145
  scoreInteractive,
2146
+ scoreLayoutTextContrast,
1996
2147
  setDebugChannels,
1997
2148
  shouldSkipAutoSettleForInspectPick,
1998
2149
  tapBest,