peachsim 0.1.1594 → 0.1.1619

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 (224) hide show
  1. package/README.md +34 -11
  2. package/cli/app-catalog.ts +7 -5
  3. package/cli/app-config.ts +6 -2
  4. package/cli/app-fonts.ts +74 -35
  5. package/cli/app-stubs.ts +173 -42
  6. package/cli/bin.ts +9 -0
  7. package/cli/bridge-flow-runner.ts +189 -84
  8. package/cli/cloud-client.ts +26 -25
  9. package/cli/commands/agent-indicator-mode.ts +39 -0
  10. package/cli/commands/box.ts +13 -3
  11. package/cli/commands/control.ts +132 -108
  12. package/cli/commands/daemon.ts +99 -24
  13. package/cli/commands/debug.ts +29 -1
  14. package/cli/commands/detect.ts +1 -1
  15. package/cli/commands/device.ts +6 -8
  16. package/cli/commands/diagnose.ts +2 -0
  17. package/cli/commands/doctor.ts +97 -34
  18. package/cli/commands/flow.ts +22 -12
  19. package/cli/commands/inspect/actions.ts +2 -0
  20. package/cli/commands/inspect/core.ts +106 -40
  21. package/cli/commands/inspect/list.ts +9 -3
  22. package/cli/commands/inspect/memory.ts +9 -0
  23. package/cli/commands/inspect/settling.ts +167 -49
  24. package/cli/commands/inspect/shared.ts +53 -12
  25. package/cli/commands/inspect/wait-event.ts +0 -1
  26. package/cli/commands/inspect/wait.ts +2 -2
  27. package/cli/commands/inspect.ts +19 -4
  28. package/cli/commands/kotlin.ts +5 -7
  29. package/cli/commands/maestro-compare.ts +25 -2
  30. package/cli/commands/mode.ts +13 -1
  31. package/cli/commands/perf-record.ts +296 -0
  32. package/cli/commands/perf.ts +6 -1
  33. package/cli/commands/platform.ts +5 -4
  34. package/cli/commands/preview.ts +1 -1
  35. package/cli/commands/record.ts +28 -5
  36. package/cli/commands/runtime.ts +9 -11
  37. package/cli/commands/screenshots.ts +4 -3
  38. package/cli/commands/setup.ts +17 -5
  39. package/cli/commands/skills.ts +16 -43
  40. package/cli/commands/swift.ts +18 -3
  41. package/cli/commands/upgrade.ts +7 -6
  42. package/cli/commands/upload.ts +27 -51
  43. package/cli/commands/window.ts +449 -0
  44. package/cli/desktop-companion.ts +84 -10
  45. package/cli/drivers/desktop.ts +2 -0
  46. package/cli/flow-file.ts +1 -0
  47. package/cli/maestro-compare.ts +19 -3
  48. package/cli/main.ts +22 -1
  49. package/cli/material-symbol-fonts.ts +171 -0
  50. package/cli/outbound-endpoints.ts +27 -13
  51. package/cli/preview-capture.ts +2 -2
  52. package/cli/run-registry.ts +5 -2
  53. package/cli/telemetry.ts +6 -3
  54. package/cli/ws-bridge.ts +15 -19
  55. package/detox/attempt-events.cjs +63 -7
  56. package/detox/attempt-events.d.cts +7 -0
  57. package/detox/gestures.ts +115 -2
  58. package/detox/index.ts +313 -411
  59. package/detox/jest-environment.cjs +86 -0
  60. package/detox/jest-preset.cjs +3 -1
  61. package/detox/jest-setup-after-env.cjs +1 -0
  62. package/detox/matchers.ts +3 -6
  63. package/detox/motion-change.cjs +9 -1
  64. package/detox/proof-frame.cjs +83 -9
  65. package/dist-lib/agent-daemon-client.cjs +1 -1
  66. package/dist-lib/agent-events.cjs +1 -1
  67. package/dist-lib/attached-projects.cjs +1 -1
  68. package/dist-lib/auth/shared-session.cjs +15 -5
  69. package/dist-lib/backend-origin.cjs +1 -1
  70. package/dist-lib/beta.cjs +1 -1
  71. package/dist-lib/beta.mjs +1 -1
  72. package/dist-lib/bridge-constants.cjs +1 -1
  73. package/dist-lib/bridge-contract-input.cjs +1 -1
  74. package/dist-lib/bridge-contract-input.mjs +1 -1
  75. package/dist-lib/bridge-contract.cjs +1 -1
  76. package/dist-lib/bridge-contract.mjs +1 -1
  77. package/dist-lib/bundle-channel.cjs +4000 -1362
  78. package/dist-lib/capture-contract.cjs +1 -1
  79. package/dist-lib/capture-contract.mjs +1 -1
  80. package/dist-lib/cloud-contract.cjs +6 -1
  81. package/dist-lib/cloud-contract.mjs +5 -1
  82. package/dist-lib/cloud-test-args.cjs +1 -1
  83. package/dist-lib/cloud-test-args.mjs +1 -1
  84. package/dist-lib/cloud.cjs +1 -1
  85. package/dist-lib/cloud.mjs +1 -1
  86. package/dist-lib/config.cjs +26 -7
  87. package/dist-lib/config.mjs +26 -7
  88. package/dist-lib/detox/index.cjs +1908 -539
  89. package/dist-lib/detox/jest-environment.cjs +86 -0
  90. package/dist-lib/detox/jest-preset.cjs +3 -1
  91. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  92. package/dist-lib/expo-swift-bundle.cjs +3612 -1016
  93. package/dist-lib/expo-swift-metadata.cjs +1 -1
  94. package/dist-lib/expo-swift-metadata.mjs +1 -1
  95. package/dist-lib/home-paths.cjs +1 -1
  96. package/dist-lib/host/bridge-host.cjs +4755 -1825
  97. package/dist-lib/host/fetch-proxy-handler.cjs +59 -9
  98. package/dist-lib/host/fetch-proxy-overrides.cjs +62 -2
  99. package/dist-lib/host/fetch-proxy-overrides.mjs +56 -2
  100. package/dist-lib/host/replacement-module-handler.cjs +8 -8
  101. package/dist-lib/host/websocket-proxy.cjs +22 -6
  102. package/dist-lib/index.cjs +603 -184
  103. package/dist-lib/jump-to-source-babel.cjs +1 -1
  104. package/dist-lib/jump-to-source-native.cjs +33 -4
  105. package/dist-lib/kotlin-host.cjs +262 -121
  106. package/dist-lib/menu.cjs +1 -1
  107. package/dist-lib/menu.mjs +1 -1
  108. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  109. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  110. package/dist-lib/metro-production-bundle.cjs +101 -7
  111. package/dist-lib/metro-production-bundle.mjs +106 -7
  112. package/dist-lib/metro.cjs +308 -87
  113. package/dist-lib/native-app-identity-project.cjs +99 -26
  114. package/dist-lib/native-app-identity-project.mjs +99 -26
  115. package/dist-lib/native-app-identity.cjs +25 -7
  116. package/dist-lib/native-app-identity.mjs +25 -7
  117. package/dist-lib/native-firebase-metadata.cjs +1 -1
  118. package/dist-lib/native-firebase-metadata.mjs +1 -1
  119. package/dist-lib/plist.cjs +1 -1
  120. package/dist-lib/plist.mjs +1 -1
  121. package/dist-lib/profiles.cjs +1 -1
  122. package/dist-lib/public-brand.cjs +1 -1
  123. package/dist-lib/react-native-host-modules.cjs +1 -1
  124. package/dist-lib/react-native-host-modules.mjs +1 -1
  125. package/dist-lib/scripts/dev-server-scanner.cjs +30 -9
  126. package/dist-lib/sdk.cjs +236 -78
  127. package/dist-lib/sdk.mjs +236 -78
  128. package/dist-lib/skills.cjs +6713 -2597
  129. package/dist-lib/swift-compile.cjs +1632 -299
  130. package/dist-lib/swift-compile.mjs +1553 -219
  131. package/dist-lib/swift-package.cjs +25 -3
  132. package/dist-lib/swift-package.mjs +22 -2
  133. package/dist-lib/swift-submit.cjs +1 -1
  134. package/dist-lib/swift-submit.mjs +1 -1
  135. package/dist-lib/swift-toolchain.cjs +1 -1
  136. package/dist-lib/swift.cjs +1 -1
  137. package/dist-lib/vite-swift.cjs +3670 -1051
  138. package/dist-lib/vite.cjs +142 -20
  139. package/dist-lib/worklet-executable.cjs +1 -1
  140. package/dist-lib/worklet-executable.mjs +1 -1
  141. package/install.sh +1 -1
  142. package/kotlin/AppSources/android/view/Window.kt +4 -1
  143. package/kotlin/AppSources/android/window/BackEvent.kt +24 -0
  144. package/kotlin/AppSources/android/window/OnBackAnimationCallback.kt +7 -0
  145. package/kotlin/AppSources/android/window/OnBackInvokedDispatcher.kt +37 -1
  146. package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +5 -0
  147. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +8 -1
  148. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +14 -0
  149. package/kotlin/CompilerSources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +1 -0
  150. package/kotlin/CompilerSources/PeachJniIncoming.kt +243 -0
  151. package/kotlin/CompilerSources/PeachJniPlugin.kt +116 -0
  152. package/kotlin/ComposeSources/PeachComposeHost.kt +27 -20
  153. package/kotlin/JniPlatformSources/androidx/annotation/CallSuper.kt +5 -0
  154. package/kotlin/JniPlatformSources/com/facebook/proguard/annotations/DoNotStrip.kt +5 -0
  155. package/kotlin/JniPlatformSources/dalvik/annotation/optimization/FastNative.kt +5 -0
  156. package/kotlin/JniPlatformSources/kotlin/Synchronized.kt +6 -0
  157. package/kotlin/JniPlatformSources/kotlin/jvm/JvmField.kt +5 -0
  158. package/kotlin/JniPlatformSources/kotlin/jvm/JvmStatic.kt +5 -0
  159. package/kotlin/JniSources/com/facebook/jni/HybridData.kt +68 -0
  160. package/kotlin/JniSources/dev/peach/jni/IncomingBindings.kt +157 -0
  161. package/kotlin/JniSources/dev/peach/jni/NativeBindings.kt +79 -0
  162. package/kotlin/JniTransportSources/dev/peach/jni/integration/JniTransport.kt +91 -0
  163. package/kotlin/README.md +18 -0
  164. package/package.json +1 -1
  165. package/scripts/dev-server-scanner.ts +30 -40
  166. package/skills/contrast/SKILL.md +1 -1
  167. package/skills/peach-perf/SKILL.md +25 -0
  168. package/skills/peach-setup/SKILL.md +25 -1
  169. package/src/assetCatalogContract.ts +31 -3
  170. package/src/auth/shared-session.ts +3 -2
  171. package/src/bridge-contract.ts +5 -0
  172. package/src/bundle-channel.mjs +1 -1
  173. package/src/cloud-contract.ts +11 -0
  174. package/src/config.ts +16 -8
  175. package/src/expo-kotlin-host.ts +7 -2
  176. package/src/guest-bundle.ts +15 -8
  177. package/src/host/agent-host.ts +2 -4
  178. package/src/host/bridge-host.ts +52 -54
  179. package/src/host/fetch-proxy-handler.ts +94 -31
  180. package/src/host/fetch-proxy-overrides.ts +91 -0
  181. package/src/host/replacement-module-handler.ts +11 -10
  182. package/src/host/server-scan-handler.ts +1 -1
  183. package/src/host/staged-host-files.ts +92 -0
  184. package/src/host/websocket-proxy.ts +27 -6
  185. package/src/installed-package-versions.ts +1 -0
  186. package/src/jump-to-source-native.ts +3 -3
  187. package/src/kotlin-host.ts +143 -26
  188. package/src/kotlin-module.ts +52 -0
  189. package/src/metro-layout-parser.ts +78 -0
  190. package/src/metro-production-bundle.ts +59 -10
  191. package/src/native-app-identity-project.ts +65 -3
  192. package/src/native-app-identity.ts +31 -0
  193. package/src/native-firebase-config.ts +3 -2
  194. package/src/native-seam-manifest.ts +0 -44
  195. package/src/nativeAppIconContract.ts +11 -6
  196. package/src/nativeViewSourceContract.ts +49 -0
  197. package/src/nativeViewWebContract.ts +41 -0
  198. package/src/nitro-source.ts +3 -7
  199. package/src/one-widget-metadata.ts +23 -3
  200. package/src/oneWidgetContract.ts +22 -0
  201. package/src/process-group.ts +306 -0
  202. package/src/process-liveness.ts +24 -0
  203. package/src/process-output.ts +21 -2
  204. package/src/serverDiscoveryContract.ts +41 -0
  205. package/src/swift-compile.ts +1057 -265
  206. package/src/swift-design-time.ts +552 -0
  207. package/src/swift-distribution.lock.json +12 -11
  208. package/src/swift-host.ts +133 -25
  209. package/src/swift-objc-sdk.ts +726 -0
  210. package/src/swift-objc-standin.ts +441 -0
  211. package/src/swift-package.ts +25 -0
  212. package/src/swift-packages.ts +25 -10
  213. package/src/swift-standin-config.ts +94 -0
  214. package/src/swift-standin-emit.ts +5 -3
  215. package/src/swift-standins.ts +225 -81
  216. package/src/swiftContract.ts +4 -0
  217. package/src/target-matcher.ts +67 -0
  218. package/src/visit-parsed-program.ts +21 -0
  219. package/src/vite-plugin-one.ts +58 -16
  220. package/src/vite-plugin-swift.ts +1 -0
  221. package/src/vite-plugin.ts +105 -41
  222. package/src/worker-system-chunks.ts +86 -9
  223. package/src/worklets-babel.ts +85 -13
  224. package/src/xcode-project.ts +40 -15
@@ -0,0 +1,157 @@
1
+ package dev.peach.jni
2
+
3
+ private data class IncomingKey(val owner: String, val name: String, val descriptor: String, val isStatic: Boolean)
4
+ private data class IncomingClass(val parents: Array<String>, val accepts: (Any?) -> Boolean)
5
+ private data class IncomingMethod(
6
+ val key: IncomingKey,
7
+ val arguments: Int,
8
+ val virtual: (Any?, Array<Any?>) -> Any?,
9
+ val nonvirtual: ((Any?, Array<Any?>) -> Any?)?,
10
+ )
11
+ private data class IncomingField(val key: IncomingKey, val read: (Any?) -> Any?, val write: ((Any?, Any?) -> Unit)?)
12
+ private val incomingClasses = mutableMapOf<String, IncomingClass>()
13
+ private val incomingMethods = mutableListOf<IncomingMethod>()
14
+ private val incomingMethodIds = mutableMapOf<IncomingKey, Int>()
15
+ private val incomingFields = mutableListOf<IncomingField>()
16
+ private val incomingFieldIds = mutableMapOf<IncomingKey, Int>()
17
+ private var incomingInitialized = false
18
+ private var incomingRetired = false
19
+
20
+ // the compiler replaces this body with registrations of original declarations.
21
+ internal fun initializeIncomingBindings() {
22
+ error("JNI incoming bindings require the compiler plugin")
23
+ }
24
+
25
+ private fun requireIncomingBindings() {
26
+ check(!incomingRetired) { "native binding runtime retired" }
27
+ if (!incomingInitialized) {
28
+ initializeIncomingBindings()
29
+ incomingInitialized = true
30
+ }
31
+ }
32
+
33
+ internal fun registerIncomingClass(owner: String, parents: Array<String>, accepts: (Any?) -> Boolean) {
34
+ check(incomingClasses.put(owner, IncomingClass(parents, accepts)) == null) { "duplicate incoming class: $owner" }
35
+ }
36
+
37
+ internal fun registerIncomingMethod(
38
+ owner: String, name: String, descriptor: String, isStatic: Boolean, arguments: Int,
39
+ virtual: (Any?, Array<Any?>) -> Any?, nonvirtual: ((Any?, Array<Any?>) -> Any?)?,
40
+ ) {
41
+ val key = IncomingKey(owner, name, descriptor, isStatic)
42
+ check(key !in incomingMethodIds) { "duplicate incoming method: $owner.$name$descriptor" }
43
+ incomingMethods += IncomingMethod(key, arguments, virtual, nonvirtual)
44
+ incomingMethodIds[key] = incomingMethods.size
45
+ }
46
+
47
+ internal fun registerIncomingField(owner: String, name: String, descriptor: String, isStatic: Boolean, read: (Any?) -> Any?, write: ((Any?, Any?) -> Unit)?) {
48
+ val key = IncomingKey(owner, name, descriptor, isStatic)
49
+ check(key !in incomingFieldIds) { "duplicate incoming field: $owner.$name:$descriptor" }
50
+ incomingFields += IncomingField(key, read, write)
51
+ incomingFieldIds[key] = incomingFields.size
52
+ }
53
+
54
+ fun requireIncomingClass(owner: String) {
55
+ requireIncomingBindings()
56
+ if (owner !in incomingClasses) throw ClassNotFoundException(owner)
57
+ }
58
+
59
+ fun isIncomingInstance(owner: String, receiver: Any?): Boolean {
60
+ requireIncomingClass(owner)
61
+ return incomingClasses.getValue(owner).accepts(receiver)
62
+ }
63
+
64
+ fun isIncomingAssignableFrom(target: String, source: String): Boolean {
65
+ requireIncomingClass(target)
66
+ requireIncomingClass(source)
67
+ fun inherits(type: String): Boolean = type == target || incomingClasses[type]?.parents.orEmpty().any(::inherits)
68
+ return inherits(source)
69
+ }
70
+
71
+ fun incomingClassOf(receiver: Any): String {
72
+ requireIncomingBindings()
73
+ val matches = incomingClasses.filterValues { it.accepts(receiver) }.keys
74
+ return matches.firstOrNull { candidate -> matches.none { it != candidate && isIncomingAssignableFrom(candidate, it) } }
75
+ ?: throw ClassNotFoundException("receiver has no registered class")
76
+ }
77
+
78
+ private fun lookupIncoming(owner: String, name: String, descriptor: String, isStatic: Boolean, ids: Map<IncomingKey, Int>): Int? {
79
+ val key = IncomingKey(owner, name, descriptor, isStatic)
80
+ ids[key]?.let { return it }
81
+ if (name == "<init>") return null
82
+ for (parent in incomingClasses[owner]?.parents.orEmpty()) {
83
+ lookupIncoming(parent, name, descriptor, isStatic, ids)?.let { return it }
84
+ }
85
+ return null
86
+ }
87
+
88
+ // ids describe declarations; a cached base id remains valid on derived instances.
89
+ fun lookupIncomingMethod(owner: String, name: String, descriptor: String, isStatic: Boolean = false): Int {
90
+ requireIncomingClass(owner)
91
+ return lookupIncoming(owner, name, descriptor, isStatic, incomingMethodIds)
92
+ ?: throw NoSuchMethodError("$owner.$name$descriptor")
93
+ }
94
+
95
+ fun lookupIncomingField(owner: String, name: String, descriptor: String, isStatic: Boolean = false): Int {
96
+ requireIncomingClass(owner)
97
+ return lookupIncoming(owner, name, descriptor, isStatic, incomingFieldIds)
98
+ ?: throw NoSuchFieldError("$owner.$name:$descriptor")
99
+ }
100
+
101
+ private fun requireIncomingReceiver(key: IncomingKey, receiver: Any?) {
102
+ if (!key.isStatic && key.name != "<init>") {
103
+ if (receiver == null) throw NullPointerException("null receiver for ${key.owner}.${key.name}")
104
+ if (!incomingClasses.getValue(key.owner).accepts(receiver))
105
+ throw IllegalArgumentException("receiver is not an instance of ${key.owner}")
106
+ }
107
+ }
108
+
109
+ fun invokeIncoming(id: Int, receiver: Any?, arguments: Array<Any?>, nonvirtual: Boolean = false): Any? {
110
+ requireIncomingBindings()
111
+ val method = incomingMethods.getOrNull(id - 1) ?: throw NoSuchMethodError("invalid incoming method id: $id")
112
+ require(method.key.name != "<init>") { "constructors require constructIncoming" }
113
+ requireIncomingReceiver(method.key, receiver)
114
+ require(arguments.size == method.arguments) { "wrong argument count for ${method.key.owner}.${method.key.name}${method.key.descriptor}" }
115
+ val body = if (nonvirtual) method.nonvirtual
116
+ ?: throw AbstractMethodError("${method.key.owner}.${method.key.name}${method.key.descriptor}")
117
+ else method.virtual
118
+ return body(receiver, arguments)
119
+ }
120
+
121
+ fun constructIncoming(id: Int, arguments: Array<Any?>): Any {
122
+ requireIncomingBindings()
123
+ val method = incomingMethods.getOrNull(id - 1) ?: throw NoSuchMethodError("invalid incoming constructor id: $id")
124
+ require(method.key.name == "<init>" && !method.key.isStatic) { "method is not a constructor" }
125
+ require(arguments.size == method.arguments) { "wrong constructor argument count for ${method.key.owner}${method.key.descriptor}" }
126
+ return checkNotNull(method.virtual(null, arguments))
127
+ }
128
+
129
+ fun readIncomingField(id: Int, receiver: Any?): Any? {
130
+ requireIncomingBindings()
131
+ val field = incomingFields.getOrNull(id - 1) ?: throw NoSuchFieldError("invalid incoming field id: $id")
132
+ requireIncomingReceiver(field.key, receiver)
133
+ return field.read(receiver)
134
+ }
135
+
136
+ fun writeIncomingField(id: Int, receiver: Any?, value: Any?) {
137
+ requireIncomingBindings()
138
+ val field = incomingFields.getOrNull(id - 1) ?: throw NoSuchFieldError("invalid incoming field id: $id")
139
+ requireIncomingReceiver(field.key, receiver)
140
+ val write = field.write ?: throw IllegalAccessError("final field: ${field.key.owner}.${field.key.name}")
141
+ write(receiver, value)
142
+ }
143
+
144
+ internal fun retireIncomingBindings() {
145
+ incomingRetired = true
146
+ incomingClasses.clear()
147
+ incomingMethods.clear()
148
+ incomingMethodIds.clear()
149
+ incomingFields.clear()
150
+ incomingFieldIds.clear()
151
+ }
152
+
153
+ class ClassNotFoundException(message: String) : Exception(message)
154
+ class NoSuchMethodError(message: String) : Error(message)
155
+ class NoSuchFieldError(message: String) : Error(message)
156
+ class AbstractMethodError(message: String) : Error(message)
157
+ class IllegalAccessError(message: String) : Error(message)
@@ -0,0 +1,79 @@
1
+ @file:OptIn(kotlin.js.ExperimentalWasmJsInterop::class)
2
+
3
+ package dev.peach.jni
4
+
5
+ import kotlin.js.*
6
+
7
+ private data class Method(val owner: String, val name: String, val descriptor: String)
8
+ private val methods = mutableMapOf<Method, (Any?, Array<Any?>) -> Any?>()
9
+ private var nativeRuntime: NativeRuntime? = null
10
+ private var retired = false
11
+ private var retiring = false
12
+ private val retirementActions = mutableListOf<() -> Unit>()
13
+ private fun assertActive() = check(!retired) { "native binding runtime retired" }
14
+
15
+ fun isNativeRuntimeActive(): Boolean = !retired && !retiring
16
+
17
+ fun registerNativeRetirement(action: () -> Unit) {
18
+ check(isNativeRuntimeActive()) { "native binding runtime retired" }
19
+ retirementActions += action
20
+ }
21
+
22
+ // registration glue supplies native entrypoints; package method bodies stay unchanged.
23
+ fun registerNative(owner: String, method: String, descriptor: String, body: (Any?, Array<Any?>) -> Any?) {
24
+ check(isNativeRuntimeActive()) { "native binding runtime retired" }
25
+ val key = Method(owner, method, descriptor)
26
+ check(key !in methods) { "native method already bound: $owner.$method$descriptor" }
27
+ methods[key] = body
28
+ }
29
+
30
+ fun invoke(receiver: Any?, owner: String, method: String, descriptor: String, arguments: Array<Any?>): Any? {
31
+ assertActive()
32
+ val body = methods[Method(owner, method, descriptor)]
33
+ ?: throw UnsatisfiedLinkError("unbound native method: $owner.$method$descriptor")
34
+ return body(receiver, arguments)
35
+ }
36
+
37
+ class UnsatisfiedLinkError(message: String) : Error(message)
38
+
39
+ external interface NativePeer : JsAny {
40
+ fun reset()
41
+ fun isValid(): Boolean
42
+ }
43
+
44
+ external interface NativeCallback : JsAny {
45
+ fun invoke(arguments: JsReference<Array<Any?>>): JsReference<Any>?
46
+ fun release()
47
+ }
48
+
49
+ external interface NativeRuntime : JsAny {
50
+ fun create(owner: String, receiver: JsReference<Any>): NativePeer
51
+ fun attach(peer: NativePeer, owner: JsReference<Any>)
52
+ fun capture(peer: NativePeer, receiver: JsReference<Any>, dispatch: (JsReference<Any>, JsReference<Array<Any?>>) -> JsReference<Any>?): NativeCallback
53
+ fun retire()
54
+ }
55
+
56
+ fun installNativeRuntime(runtime: NativeRuntime) {
57
+ check(isNativeRuntimeActive()) { "native binding runtime retired" }
58
+ check(nativeRuntime == null) { "native binding runtime is already installed" }
59
+ nativeRuntime = runtime
60
+ }
61
+
62
+ fun retireNativeRuntime() {
63
+ if (retired || retiring) return
64
+ retiring = true
65
+ var failure: Throwable? = null
66
+ try {
67
+ for (action in retirementActions) {
68
+ try { action() } catch (error: Throwable) { if (failure == null) failure = error }
69
+ }
70
+ } finally {
71
+ retirementActions.clear()
72
+ retired = true
73
+ retireIncomingBindings()
74
+ nativeRuntime?.retire()
75
+ methods.clear()
76
+ }
77
+ failure?.let { throw it }
78
+ // retirement is terminal for this Kotlin process.
79
+ }
@@ -0,0 +1,91 @@
1
+ @file:OptIn(kotlin.js.ExperimentalWasmJsInterop::class)
2
+
3
+ package dev.peach.jni.integration
4
+
5
+ import dev.peach.jni.*
6
+ import kotlin.js.*
7
+
8
+ external interface JniTransportReply : JsAny {
9
+ val value: JsReference<Any>?
10
+ val error: JsReference<Throwable>?
11
+ }
12
+
13
+ private fun transportReply(value: JsReference<Any>?, error: JsReference<Throwable>?): JniTransportReply =
14
+ js("({ value, error })")
15
+
16
+ private inline fun transport(body: () -> Any?): JniTransportReply = try {
17
+ transportReply(body()?.toJsReference(), null)
18
+ } catch (error: Throwable) {
19
+ transportReply(null, error.toJsReference())
20
+ }
21
+
22
+ @JsExport fun jniMethod(owner: String, name: String, descriptor: String, isStatic: Boolean): JniTransportReply =
23
+ transport { lookupIncomingMethod(owner, name, descriptor, isStatic) }
24
+
25
+ @JsExport fun jniField(owner: String, name: String, descriptor: String, isStatic: Boolean): JniTransportReply =
26
+ transport { lookupIncomingField(owner, name, descriptor, isStatic) }
27
+
28
+ @JsExport fun jniInvoke(id: Int, receiver: JsReference<Any>?, arguments: JsReference<Array<Any?>>, nonvirtual: Boolean): JniTransportReply =
29
+ transport { invokeIncoming(id, receiver?.get(), arguments.get(), nonvirtual) }
30
+
31
+ @JsExport fun jniConstruct(id: Int, arguments: JsReference<Array<Any?>>): JniTransportReply =
32
+ transport { constructIncoming(id, arguments.get()) }
33
+
34
+ @JsExport fun jniRead(id: Int, receiver: JsReference<Any>?): JniTransportReply =
35
+ transport { readIncomingField(id, receiver?.get()) }
36
+
37
+ @JsExport fun jniWrite(id: Int, receiver: JsReference<Any>?, value: JsReference<Any>?): JniTransportReply =
38
+ transport { writeIncomingField(id, receiver?.get(), value?.get()); null }
39
+
40
+ @JsExport fun jniRequireClass(owner: String): JniTransportReply = transport { requireIncomingClass(owner); null }
41
+ @JsExport fun jniClassOf(receiver: JsReference<Any>): JniTransportReply = transport { incomingClassOf(receiver.get()) }
42
+ @JsExport fun jniInstanceOf(owner: String, receiver: JsReference<Any>?): JniTransportReply = transport { isIncomingInstance(owner, receiver?.get()) }
43
+ @JsExport fun jniAssignable(target: String, source: String): JniTransportReply = transport { isIncomingAssignableFrom(target, source) }
44
+ @JsExport fun jniSame(a: JsReference<Any>?, b: JsReference<Any>?): Boolean = a?.get() === b?.get()
45
+
46
+ @JsExport fun jniArguments(count: Int): JsReference<Array<Any?>> = arrayOfNulls<Any>(count).toJsReference()
47
+ @JsExport fun jniArgument(arguments: JsReference<Array<Any?>>, index: Int, value: JsReference<Any>?) { arguments.get()[index] = value?.get() }
48
+ @JsExport fun jniArgumentAt(arguments: JsReference<Array<Any?>>, index: Int): JsReference<Any>? = arguments.get()[index]?.toJsReference()
49
+ @JsExport fun jniArgumentCount(arguments: JsReference<Array<Any?>>): Int = arguments.get().size
50
+ @JsExport fun jniInt(value: JsReference<Any>): Int = when (val scalar = value.get()) {
51
+ is Int -> scalar
52
+ is Byte -> scalar.toInt()
53
+ is Short -> scalar.toInt()
54
+ is Char -> scalar.code
55
+ else -> error("JNI value is not an integer scalar")
56
+ }
57
+ @JsExport fun jniLong(value: JsReference<Any>): String = (value.get() as Long).toString()
58
+ @JsExport fun jniString(value: JsReference<Any>): String = value.get() as String
59
+ @JsExport fun jniBoolean(value: JsReference<Any>): Boolean = value.get() as Boolean
60
+ @JsExport fun jniNumber(value: JsReference<Any>): Double = when (val scalar = value.get()) {
61
+ is Float -> scalar.toDouble()
62
+ is Double -> scalar
63
+ else -> error("JNI value is not a floating-point scalar")
64
+ }
65
+ @JsExport fun jniPrimitive(descriptor: String, integer: String, number: Double, text: String): JniTransportReply = transport {
66
+ when (descriptor) {
67
+ "Z" -> when (integer) { "0" -> false; "1" -> true; else -> error("invalid JNI boolean") }
68
+ "B" -> integer.toByte()
69
+ "C" -> integer.toInt().also { require(it in 0..65535) }.toChar()
70
+ "S" -> integer.toShort()
71
+ "I" -> integer.toInt()
72
+ "J" -> integer.toLong()
73
+ "F" -> number.toFloat()
74
+ "D" -> number
75
+ "Ljava/lang/String;" -> text
76
+ else -> error("unsupported JNI scalar descriptor: $descriptor")
77
+ }
78
+ }
79
+ @JsExport fun jniErrorClass(error: JsReference<Throwable>): String? = error.get()::class.simpleName
80
+ @JsExport fun jniErrorMessage(error: JsReference<Throwable>): String? = error.get().message
81
+
82
+ @JsExport fun jniBind(owner: String, name: String, descriptor: String,
83
+ callback: (JsReference<Any>?, JsReference<Array<Any?>>) -> JniTransportReply): JniTransportReply = transport {
84
+ registerNative(owner, name, descriptor) { receiver, arguments ->
85
+ val result = callback(receiver?.toJsReference(), arguments.toJsReference())
86
+ result.error?.let { throw it.get() }
87
+ result.value?.get()
88
+ }
89
+ null
90
+ }
91
+ @JsExport fun jniRetire(): JniTransportReply = transport { retireNativeRuntime(); null }
package/kotlin/README.md CHANGED
@@ -31,6 +31,24 @@ traffic in `scripts/debug/peach-swift-lazy-network.sh`.
31
31
  - `ExpoSources/`: the expo-modules-core Kotlin DSL port and `PeachExpoRuntime`
32
32
  that local Expo Android modules compile against. Owner spec:
33
33
  [`specs/peach-kotlin-expo-modules.md`](../../../specs/peach-kotlin-expo-modules.md).
34
+ - `CompilerSources/`: the native member compiler plugin and its JVM service
35
+ registration. The shared host compiles it into a classes directory cached by
36
+ the pinned compiler and every plugin source byte. Completed directories publish
37
+ atomically under `~/.peach/kotlin/compiler-plugins/`; all Kotlin entry paths
38
+ load that plugin. `JniPlatformSources/` and `JniSources/` supply the shared
39
+ platform declarations and native invocation runtime for imported modules,
40
+ pure Kotlin apps and Expo modules.
41
+ Incoming JNI lookup has stable method and field IDs, declaration-backed private
42
+ and virtual calls, actual constructors and backing-field access. Native static
43
+ companion calls preserve the enclosing JVM owner and null receiver. The
44
+ [binding contract](../../../docs/research/peach/kotlin-native-bindings.md)
45
+ owns the APIs, holder lifetime and unsupported forms.
46
+
47
+ App artifact hashes include compiler and library pins, plugin sources, platform
48
+ and runtime sources, app sources and generated glue. The host compiles byte
49
+ snapshots of the hashed app, platform and plugin inputs, preserving their
50
+ original paths in compiler diagnostics. A cached artifact needs no compiler
51
+ process; another app reuses the compiled plugin without rebuilding it.
34
52
 
35
53
  The dev server compiles every `.kt` source of the app, One's glue for each and
36
54
  a generated entry into one Kotlin/Wasm module, held to the Kotlin language
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peachsim",
3
- "version": "0.1.1594",
3
+ "version": "0.1.1619",
4
4
  "description": "Vite and Metro plugins, testing drivers, and SDK exports for peach.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -44,8 +44,9 @@ import {
44
44
  parseSwiftHostDiscovery,
45
45
  } from '../src/swiftDiscoveryContract'
46
46
  import { loadOptionalDemoApps, optionalDemoAppsSync } from './optional-demo-registry.ts'
47
- import type { ConfiguredOneWidget } from '../src/oneWidgetContract'
48
- import type { AppSettingsSpecifier } from '@peach/globals'
47
+ import type { DiscoveredServer } from '../src/serverDiscoveryContract'
48
+
49
+ export type { DiscoveredServer } from '../src/serverDiscoveryContract'
49
50
 
50
51
  // one machine-wide lock. lsof walks every open fd, so overlapping scans from
51
52
  // every peach process are what drives the load average through the roof.
@@ -297,37 +298,6 @@ function spawnReaped(
297
298
  )
298
299
  }
299
300
 
300
- export interface DiscoveredServer {
301
- port: number
302
- framework: 'metro' | 'expo' | 'vxrn' | 'one' | 'swift' | 'kotlin' | 'unknown'
303
- status?: 'compiling' | 'ready' | 'error'
304
- projectName?: string
305
- bundleUrl: string
306
- hmrUrl?: string
307
- lastSeen: number
308
- iconUrl?: string
309
- iconPath?: string
310
- /** the app's own brand colour from its manifest, used as the home tile's
311
- * background so the tile reads as that app before its icon decodes and
312
- * when the icon is missing entirely. */
313
- primaryColor?: string
314
- bundleId?: string
315
- widget?: ConfiguredOneWidget | null
316
- settingsBundle?: readonly AppSettingsSpecifier[]
317
- settingsStorageScopeUrl?: string
318
- patched?: boolean
319
- /** true when `bundleUrl` was NOT derived from the server's own manifest —
320
- * the manifest probe got no answer, so this is the generic `/index.bundle`
321
- * guess. good enough to LIST the server; never good enough to open it. see
322
- * `manifestState` below for why the two are different answers. */
323
- bundleUrlProvisional?: boolean
324
- /** absolute cwd of the owning node/bun process — resolved from `lsof -d cwd`
325
- * when the scanner can see the pid. used to auto-attach the project for
326
- * agent sessions without requiring a manual `sootsim agent attach`. */
327
- cwd?: string
328
- pid?: number
329
- }
330
-
331
301
  /** how the manifest probe (`GET /` with expo-platform) actually went. the same
332
302
  * three answers `dev-bundle-resolution.ts` uses, deliberately spelled the same
333
303
  * way: a second vocabulary for one question is how the two resolvers drift.
@@ -876,14 +846,18 @@ function makeResult(
876
846
  }
877
847
 
878
848
  function withRuntimeConfig(port: number, bundleUrl: string, projectCwd?: string): string {
849
+ // bun's realpathSync opens the directory, and another process's cwd can sit on a volume
850
+ // whose privacy grant blocks that open behind a pending prompt, so only resolve it when
851
+ // the internal registry has an app to match. a published install has none.
852
+ const demoApps = optionalDemoAppsSync()
879
853
  let physicalProjectCwd: string | undefined
880
- if (projectCwd) {
854
+ if (projectCwd && demoApps.length > 0) {
881
855
  try {
882
856
  physicalProjectCwd = realpathSync(resolve(projectCwd))
883
857
  } catch {}
884
858
  }
885
859
  const knownApps = physicalProjectCwd
886
- ? optionalDemoAppsSync().filter((app) => {
860
+ ? demoApps.filter((app) => {
887
861
  try {
888
862
  return realpathSync(resolve(app.dir)) === physicalProjectCwd
889
863
  } catch {
@@ -982,13 +956,28 @@ function applyManifest(
982
956
  rawLaunchUrl,
983
957
  buildLoopbackProbeBaseUrl('http:', manifestRes.hostname, result.port),
984
958
  )
985
- : rawLaunchUrl
959
+ : undefined
986
960
  if (launchUrl && !result.patched && !isDirectOneBundleUrl(result.bundleUrl)) {
987
961
  result.bundleUrl = withRuntimeConfig(result.port, launchUrl, result.cwd)
988
962
  }
989
- const localIcon = client.icon || client.ios?.icon
990
- const remoteIcon = client.iconUrl || client.ios?.iconUrl
991
- const rawIcon = localIcon || remoteIcon
963
+ // expo's withIosIcons.ts selects its base icon as light || dark || tinted.
964
+ // normalize before publishing iconPath, which local asset probes consume.
965
+ const iosIcon: unknown = client.ios?.icon
966
+ const defaultIosIcon =
967
+ iosIcon && typeof iosIcon === 'object'
968
+ ? ('light' in iosIcon && iosIcon.light) ||
969
+ ('dark' in iosIcon && iosIcon.dark) ||
970
+ ('tinted' in iosIcon && iosIcon.tinted)
971
+ : iosIcon
972
+ const icons: unknown[] = [
973
+ defaultIosIcon,
974
+ client.icon,
975
+ client.ios?.iconUrl,
976
+ client.iconUrl,
977
+ ]
978
+ const rawIcon = icons.find(
979
+ (icon): icon is string => typeof icon === 'string' && icon.trim().length > 0,
980
+ )
992
981
  if (rawIcon) {
993
982
  result.iconPath = rawIcon
994
983
  let resolvedIconUrl: string | undefined
@@ -1483,7 +1472,8 @@ export async function probePort(
1483
1472
  oneRes &&
1484
1473
  oneRes.statusCode > 0 &&
1485
1474
  oneRes.statusCode < 400 &&
1486
- /application\/javascript/i.test(oneRes.contentType || '')
1475
+ // one's native dev engine serves text/javascript; metro uses application/javascript.
1476
+ /^(?:application|text)\/javascript(?:\s*;|$)/i.test(oneRes.contentType?.trim() || '')
1487
1477
  ) {
1488
1478
  knownNonPatched.add(port)
1489
1479
  knownOne.add(port)
@@ -39,7 +39,7 @@ automation, inspecting the render tree, and managing sims.
39
39
  | `peach do` | drive the app (tap, type, scroll, swipe, key, reload, …) |
40
40
  | `peach wait` | block until the runtime reaches a known state |
41
41
  | `peach shell` | drive shell state and simulator chrome |
42
- | `peach mode` | toggle screenshot, touch indicators, and 3D display modes |
42
+ | `peach mode` | toggle screenshot, touch indicators, the agent indicator, and 3D display modes |
43
43
  | `peach device` | inspect or change the live device preset for a sim |
44
44
  | `peach network` | inspect live network traffic from the running peach worker |
45
45
  | `peach logs` | inspect live console output from the running peach worker |
@@ -62,6 +62,31 @@ indict, remeasure — and every wasted week came from skipping a step.
62
62
 
63
63
  ## measuring
64
64
 
65
+ ### record a Performance timeline
66
+
67
+ `peach perf record` and the devtools Performance tab share one recording.
68
+ Start from either, interact with the app, then Stop from either. The saved
69
+ category choice is used unless the command supplies a preset or category list:
70
+
71
+ ```sh
72
+ peach perf record start --preset default
73
+ peach perf record status --json
74
+ peach perf record stop -o /tmp/app.profile.jsonl.gz
75
+ ```
76
+
77
+ Default records compositor frames, Animated and Reanimated animation runs,
78
+ interactions, network and logs. `--categories frames,animations,react` selects
79
+ specific categories. React attribution and memory cost more than Default;
80
+ Paint Detail changes the renderer's path, so do not compare those frame
81
+ times with a Default run. A recording retains its last five minutes and
82
+ reports gaps when frame coverage is lost. Stop releases its profiling resources.
83
+ Save writes the preview timeline’s gzip JSONL format. Open reads it, plain
84
+ JSONL, or a CLI JSON profile; a `.json` CLI output keeps the JSON form. Both
85
+ forms include frame columns, animation spans, discrete events, missed display
86
+ ticks and optional value samples. Upload shares the profile alongside the
87
+ currently loaded app through the preview uploader. This is a diagnostic capture; use the independent
88
+ presentation evidence below to make smoothness claims.
89
+
65
90
  ### presentation proof must stay outside the simulator
66
91
 
67
92
  Performance acceptance must not use `peach record`, `liveComposite`,
@@ -74,6 +74,28 @@ debugging or `/peach-test` for writing tests.
74
74
  saved sim to reuse. verify the development server and Peach runtime, then
75
75
  retry once after fixing the cause.
76
76
 
77
+ ## choose the runtime
78
+
79
+ on Apple silicon macOS and Linux x64, recommend trying the optional native
80
+ runtime for faster simulation. it is beta, so the user may run into more issues
81
+ than in the browser. keep the current default unless the user chooses native:
82
+
83
+ ```sh
84
+ peach desktop install
85
+ peach open 8081 --driver desktop
86
+ ```
87
+
88
+ use the app's actual development server port. the Linux desktop app needs a
89
+ desktop session; its `.deb` installer uses `sudo apt-get install`.
90
+ `peach desktop install --native` on Linux installs the standalone headless host
91
+ instead of the desktop app. Intel Mac and Linux arm64 use the browser.
92
+ `peach open 8081 --driver playwright --headed` explicitly opens a browser window.
93
+ pass the selected driver when opening the app in the steps below; installing
94
+ the desktop app alone does not change the saved window preference.
95
+
96
+ the native runtime renders the simulator. the background service below keeps
97
+ the bridge ready for either runtime and is a separate optional choice.
98
+
77
99
  ## first move
78
100
 
79
101
  ```sh
@@ -97,11 +119,13 @@ scripts should use `peach open <port-or-url>` so the target is explicit.
97
119
 
98
120
  if you do not know the port, run `peach open` with no target and pick from
99
121
  the detected dev servers. for hosted or unusual targets, pass the URL
100
- explicitly:
122
+ explicitly. a built bundle file needs no dev server; `peach help open` shows
123
+ the build command for One, Expo and React Native:
101
124
 
102
125
  ```sh
103
126
  peach open 8081 # load a metro dev server
104
127
  peach open https://my-app.local # load a hosted bundle
128
+ peach open ./dist/ios.js # load a built production bundle, no dev server
105
129
  ```
106
130
 
107
131
  ## you're done when
@@ -54,18 +54,46 @@ export interface SfSymbolShaderImage {
54
54
  readonly pixels: Uint8Array
55
55
  }
56
56
 
57
- // UIImage's ordered monochrome layers and unrounded content bearings.
57
+ // UIImage's ordered monochrome layers and unrounded content bearings. a layer
58
+ // with a variable threshold turns off below it; one without never does.
58
59
  export interface SfSymbolImageDrawing {
59
60
  referencePointSize: number
60
61
  basePointSize: number
61
62
  baselineOffsetUnrounded: number
62
63
  referenceCanvasSize: readonly [number, number]
63
64
  insets: readonly [number, number, number, number]
64
- layers: readonly { d: string; opacity: number; eraser: boolean }[]
65
+ layers: readonly { d: string; opacity: number; eraser: boolean; threshold?: number }[]
66
+ // an image view reflects a flippable drawing under right to left
67
+ flippable?: boolean
68
+ // automatic variable mode draws the value along the paths rather than
69
+ // turning layers off by threshold
70
+ variableDraw?: boolean
71
+ // coreui's own right to left rendition, which replaces the drawing there
72
+ rightToLeft?: SfSymbolImageDrawing
65
73
  }
66
74
 
67
- export interface SfSymbolImageMetadata extends Omit<SfSymbolImageDrawing, 'layers'> {
75
+ // a symbol's palette layers, bottom first, in its monochrome outline's space.
76
+ // level is the palette slot a layer paints in (0 primary, 1 secondary, 2
77
+ // tertiary); a layer with a variable threshold turns off below it, as its
78
+ // image layer does.
79
+ export interface SfSymbolPalette {
80
+ layers: readonly { d: string; level: number; threshold?: number }[]
81
+ }
82
+
83
+ export interface SfSymbolImageMetadata extends Omit<
84
+ SfSymbolImageDrawing,
85
+ 'layers' | 'rightToLeft'
86
+ > {
68
87
  layers: readonly { opacity: number; eraser: boolean }[]
88
+ rightToLeft?: SfSymbolImageMetadata
89
+ }
90
+
91
+ // the drawing a symbol image view shows in its laid out direction
92
+ export function sfSymbolImageForDirection(
93
+ drawing: SfSymbolImageMetadata,
94
+ rightToLeft: boolean,
95
+ ): SfSymbolImageMetadata {
96
+ return rightToLeft && drawing.rightToLeft ? drawing.rightToLeft : drawing
69
97
  }
70
98
 
71
99
  export function sfSymbolImageDimensions(
@@ -11,6 +11,7 @@ import {
11
11
  } from 'node:fs'
12
12
  import { basename, dirname, join, resolve } from 'node:path'
13
13
  import { electronUserDataDir } from '../home-paths.ts'
14
+ import { peachPublicBrand } from '../public-brand.ts'
14
15
 
15
16
  export type SharedDesktopAuthTeamSummary = {
16
17
  id: string
@@ -61,7 +62,7 @@ export function sharedDesktopAuthAccounts(
61
62
 
62
63
  const SESSION_VERSION = 1 as const
63
64
  const SESSION_FILE_ENV = 'SOOTSIM_SHARED_AUTH_FILE'
64
- const DEFAULT_ORIGIN = 'https://contrast.dev'
65
+ const DEFAULT_ORIGIN = peachPublicBrand.origin
65
66
 
66
67
  function getBaseDir() {
67
68
  const explicit = process.env[SESSION_FILE_ENV]
@@ -186,7 +187,7 @@ export function writeSharedDesktopAuthSession(
186
187
  version: SESSION_VERSION,
187
188
  token,
188
189
  user: session.user ? normalizeUser(session.user) : null,
189
- origin: session.origin?.trim() || DEFAULT_ORIGIN,
190
+ origin: session.origin?.trim() || same?.origin || DEFAULT_ORIGIN,
190
191
  source: same?.source ?? session.source,
191
192
  updatedAt: session.updatedAt || new Date().toISOString(),
192
193
  validatedAt: session.validatedAt ?? same?.validatedAt,
@@ -14,6 +14,7 @@ import type {
14
14
  SootSimCaptureFilter,
15
15
  SootSimExternalAppLifecycleOptions,
16
16
  SootSimStorageResetStrategy,
17
+ TextInputEditCommand,
17
18
  } from '@peach/globals'
18
19
 
19
20
  export type BridgeCommandType =
@@ -67,6 +68,9 @@ export interface WsCommand {
67
68
  acquireLock?: boolean
68
69
  // bridge:claim — take the lease even when another cli holds it.
69
70
  force?: boolean
71
+ // bridge:list-sims — also report the retained disconnected sims (readyState
72
+ // closed with disconnectCause) after the live ones. read in bridge-host.
73
+ includeRetained?: boolean
70
74
  // flowStatus — live flow-runner progress payload (see flow-run-store)
71
75
  status?: Record<string, unknown>
72
76
  code?: string
@@ -79,6 +83,7 @@ export interface WsCommand {
79
83
  target?: SootSimAgentActionDetail['target']
80
84
  action?: string
81
85
  text?: string
86
+ edit?: TextInputEditCommand
82
87
  appearance?: 'light' | 'dark'
83
88
  url?: string
84
89
  includeVisual?: boolean