agent-device 0.21.20 → 0.21.22

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 (357) hide show
  1. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.20.apk → agent-device-android-ime-helper-0.21.22.apk} +0 -0
  2. package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.22.apk.sha256 +1 -0
  3. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.20.manifest.json → agent-device-android-ime-helper-0.21.22.manifest.json} +4 -4
  4. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.22.apk +0 -0
  5. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.22.apk.sha256 +1 -0
  6. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.20.manifest.json → agent-device-android-snapshot-helper-0.21.22.manifest.json} +6 -6
  7. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/AppWindowScreenshot.swift +73 -0
  8. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/BackgroundInteraction.swift +557 -0
  9. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/GhostCursor.swift +141 -0
  10. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift +107 -3
  11. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/main.swift +136 -22
  12. package/apple/macos-helper/Tests/AgentDeviceMacOSHelperTests/BackgroundInteractionTests.swift +88 -0
  13. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +121 -4
  14. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +10 -2
  15. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +36 -9
  16. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +28 -0
  17. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +27 -0
  18. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedCommitDeadline.swift +6 -1
  19. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +7 -2
  20. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedInteraction.swift +21 -7
  21. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedTextEntry.swift +121 -17
  22. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +179 -6
  23. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryConfirmation.swift +119 -0
  24. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryFocus.swift +8 -1
  25. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryReadiness.swift +23 -11
  26. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextInputProbe.swift +59 -6
  27. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextTyping.swift +112 -23
  28. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TypeExecution.swift +25 -12
  29. package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/RecordingExportSupport.swift +6 -1
  30. package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +43 -13
  31. package/dist/src/adapter.js +1 -1
  32. package/dist/src/adb-executor.js +2 -2
  33. package/dist/src/adb-failure.js +1 -1
  34. package/dist/src/adb-transport.js +1 -1
  35. package/dist/src/agent-browser-lifecycle.js +1 -1
  36. package/dist/src/agent-browser-process-record.js +1 -1
  37. package/dist/src/agent-browser-provider.js +1 -1
  38. package/dist/src/agent-browser-tool.js +1 -1
  39. package/dist/src/agent-device-client.js +2 -2
  40. package/dist/src/ai-sdk.js +1 -1
  41. package/dist/src/alert-runtime.js +1 -1
  42. package/dist/src/alert.js +1 -1
  43. package/dist/src/android-adb.d.ts +12 -0
  44. package/dist/src/android-input-ownership.js +1 -1
  45. package/dist/src/app-backend.js +1 -0
  46. package/dist/src/app-catalog.js +1 -1
  47. package/dist/src/app-deployment-resolution.js +1 -1
  48. package/dist/src/app-inventory.js +1 -1
  49. package/dist/src/app-launch.js +1 -1
  50. package/dist/src/app-lifecycle.js +2 -2
  51. package/dist/src/app-lifecycle2.js +1 -1
  52. package/dist/src/app-log-files.js +1 -1
  53. package/dist/src/app-log-reconnect.js +1 -1
  54. package/dist/src/app-log-runtime2.js +3 -3
  55. package/dist/src/app-log-session-resource.js +1 -0
  56. package/dist/src/app-resolution.js +2 -2
  57. package/dist/src/apple-runner-platform.js +1 -1
  58. package/dist/src/application-lifecycle-interaction.js +1 -1
  59. package/dist/src/archive.js +2 -2
  60. package/dist/src/artifact-download.js +1 -1
  61. package/dist/src/auth-session.js +1 -1
  62. package/dist/src/auth.js +1 -1
  63. package/dist/src/back-mode.js +1 -1
  64. package/dist/src/backend-snapshot-options.js +1 -1
  65. package/dist/src/boot-diagnostics.js +1 -1
  66. package/dist/src/cache.js +1 -1
  67. package/dist/src/capabilities.js +1 -1
  68. package/dist/src/capture.js +1 -1
  69. package/dist/src/cli-config.js +1 -1
  70. package/dist/src/cli-help.js +13 -5
  71. package/dist/src/cli.js +4 -4
  72. package/dist/src/click-button.js +1 -1
  73. package/dist/src/client-metro-companion.js +1 -1
  74. package/dist/src/client-metro.js +1 -1
  75. package/dist/src/client-options.js +3 -0
  76. package/dist/src/client-types.d.ts +88 -5
  77. package/dist/src/clipboard-shell-response.js +1 -1
  78. package/dist/src/cloud-artifacts.d.ts +7 -2
  79. package/dist/src/code-signature-cache.js +1 -1
  80. package/dist/src/code-signature.js +1 -1
  81. package/dist/src/command-schema.js +1 -1
  82. package/dist/src/command-tools.js +2 -2
  83. package/dist/src/connection-runtime.js +1 -1
  84. package/dist/src/connection.js +5 -5
  85. package/dist/src/contracts.d.ts +1 -1
  86. package/dist/src/contracts.js +1 -1
  87. package/dist/src/daemon-artifacts.js +1 -0
  88. package/dist/src/daemon-client-lease-beat.js +1 -1
  89. package/dist/src/daemon-client-lifecycle.js +1 -1
  90. package/dist/src/daemon-client-liveness-probe.js +1 -0
  91. package/dist/src/daemon-client-transport.js +1 -1
  92. package/dist/src/daemon-http.js +1 -0
  93. package/dist/src/daemon-policy-file.js +1 -1
  94. package/dist/src/daemon-registration-owner.js +1 -0
  95. package/dist/src/daemon-registration.js +1 -1
  96. package/dist/src/daemon-resolution.js +1 -1
  97. package/dist/src/daemon-shutdown-report.js +1 -1
  98. package/dist/src/daemon.js +2 -2
  99. package/dist/src/debug-symbols-facade.js +1 -1
  100. package/dist/src/device-claim-inspection.js +1 -1
  101. package/dist/src/device-claim-rule.js +1 -1
  102. package/dist/src/device-inventory-context.js +1 -1
  103. package/dist/src/device-readiness-runtime.js +1 -1
  104. package/dist/src/device-release.js +1 -1
  105. package/dist/src/device-rotation.js +1 -1
  106. package/dist/src/device-selection-resolver.js +1 -1
  107. package/dist/src/device-session.js +1 -1
  108. package/dist/src/device-shell.js +1 -1
  109. package/dist/src/device.js +1 -1
  110. package/dist/src/device2.js +1 -3
  111. package/dist/src/device3.js +3 -0
  112. package/dist/src/devicectl.js +1 -1
  113. package/dist/src/diagnostics.js +3 -3
  114. package/dist/src/dispatch-resolve.js +4 -1
  115. package/dist/src/display-inventory.js +1 -1
  116. package/dist/src/doctor-facade.js +1 -1
  117. package/dist/src/doctor2.js +1 -1
  118. package/dist/src/durable-capture-resource.js +1 -0
  119. package/dist/src/engine-eval-script.js +1 -1
  120. package/dist/src/error.js +1 -1
  121. package/dist/src/exec.js +1 -1
  122. package/dist/src/file.js +1 -1
  123. package/dist/src/find.js +1 -1
  124. package/dist/src/generic-settle.js +1 -1
  125. package/dist/src/generic.js +1 -1
  126. package/dist/src/gesture-admission.js +1 -1
  127. package/dist/src/gesture-plan.js +1 -1
  128. package/dist/src/gesture-runtime.js +1 -1
  129. package/dist/src/harmonyos.js +1 -1
  130. package/dist/src/hdc.js +1 -1
  131. package/dist/src/helper.js +2 -2
  132. package/dist/src/host-file.js +1 -1
  133. package/dist/src/host.js +1 -1
  134. package/dist/src/human-control-contract.js +1 -1
  135. package/dist/src/human-control.js +1 -1
  136. package/dist/src/index.d.ts +1 -1
  137. package/dist/src/index.js +1 -1
  138. package/dist/src/input-actions.js +1 -1
  139. package/dist/src/input-actions2.js +2 -2
  140. package/dist/src/install-artifact-facade.js +1 -1
  141. package/dist/src/install-source-network-transport.js +1 -1
  142. package/dist/src/install-source.d.ts +5 -0
  143. package/dist/src/install-source2.js +1 -1
  144. package/dist/src/instance-client.js +1 -0
  145. package/dist/src/interaction-positionals.js +1 -1
  146. package/dist/src/interaction-ref-policy.js +1 -1
  147. package/dist/src/interaction.js +1 -1
  148. package/dist/src/interaction2.js +1 -1
  149. package/dist/src/interactor-operation-binding.js +1 -1
  150. package/dist/src/interactor.js +5 -5
  151. package/dist/src/interactor2.js +1 -1
  152. package/dist/src/interactors.js +1 -1
  153. package/dist/src/internal/bin.js +1 -1
  154. package/dist/src/internal/daemon.js +1 -2
  155. package/dist/src/internal/png-worker.js +1 -1
  156. package/dist/src/internal/run-script-http-child.js +1 -1
  157. package/dist/src/inventory.js +4 -4
  158. package/dist/src/inventory2.js +1 -1
  159. package/dist/src/inventory3.js +1 -1
  160. package/dist/src/inventory5.js +1 -1
  161. package/dist/src/ios-client.js +4 -30
  162. package/dist/src/ios-settings.js +1 -0
  163. package/dist/src/ios-snapshot-engine.js +1 -1
  164. package/dist/src/ios-snapshot-runtime.js +1 -1
  165. package/dist/src/launch-confirmation.js +1 -1
  166. package/dist/src/lease-context.js +1 -1
  167. package/dist/src/lease-registry-scope.js +1 -0
  168. package/dist/src/lease-scope.js +1 -1
  169. package/dist/src/lease.js +1 -1
  170. package/dist/src/legacy-xctest-device-set.js +1 -1
  171. package/dist/src/limrun-download-file.js +1 -1
  172. package/dist/src/limrun-runtime-dependencies.js +1 -1
  173. package/dist/src/limrun.d.ts +28 -2
  174. package/dist/src/limrun.js +1 -1
  175. package/dist/src/linux.js +1 -1
  176. package/dist/src/load.js +1 -0
  177. package/dist/src/location-coordinates.js +1 -1
  178. package/dist/src/macos-facade.js +1 -1
  179. package/dist/src/managed-device-scope.js +1 -1
  180. package/dist/src/mechanics.js +16 -16
  181. package/dist/src/observation.js +1 -1
  182. package/dist/src/observe-until.js +1 -1
  183. package/dist/src/overlay.js +2 -2
  184. package/dist/src/owner-identity.js +3 -3
  185. package/dist/src/path-resolution.js +1 -1
  186. package/dist/src/perf-facade.js +1 -1
  187. package/dist/src/perf-runtime-operation-builder.js +1 -1
  188. package/dist/src/perf-runtime-plan.js +1 -1
  189. package/dist/src/perf-target.js +1 -1
  190. package/dist/src/perf.js +1 -1
  191. package/dist/src/physical-device-control.js +1 -1
  192. package/dist/src/platform-request-scope.js +1 -1
  193. package/dist/src/platform-runtime-android-adb-host.js +1 -1
  194. package/dist/src/platform-runtime-app-log-process.js +1 -1
  195. package/dist/src/platform-runtime-host.js +1 -1
  196. package/dist/src/platform-runtime-managed-owner.js +1 -1
  197. package/dist/src/platform-runtime-open-target.js +1 -1
  198. package/dist/src/platform-runtime-operation-host.js +2 -2
  199. package/dist/src/platform-runtime-runtime-hints.js +2 -2
  200. package/dist/src/platform-runtime-screen-recording-android-host.js +1 -1
  201. package/dist/src/platform-runtime-screen-recording-apple-runner-transport.js +1 -1
  202. package/dist/src/platform-runtime-screen-recording-apple-simulator-host.js +1 -1
  203. package/dist/src/platform-runtime-screen-recording-finalizer-host.js +1 -1
  204. package/dist/src/platform-runtime-screen-recording-output-host.js +1 -1
  205. package/dist/src/platform-runtime-screen-recording-process-host.js +1 -1
  206. package/dist/src/platform-runtime-unavailable.js +1 -1
  207. package/dist/src/platform-runtime.js +1 -1
  208. package/dist/src/platform-runtime2.js +1 -1
  209. package/dist/src/plugins.d.ts +9 -0
  210. package/dist/src/plugins.js +1 -0
  211. package/dist/src/plugins2.js +2 -0
  212. package/dist/src/png-crop-bytes.js +1 -1
  213. package/dist/src/png-size.js +1 -1
  214. package/dist/src/png-worker-client.js +1 -1
  215. package/dist/src/png.js +1 -1
  216. package/dist/src/prepare-kind.js +1 -1
  217. package/dist/src/process.js +1 -1
  218. package/dist/src/project-runtime.js +1 -1
  219. package/dist/src/provider-credential-fingerprint.js +1 -0
  220. package/dist/src/provider-device-admission.js +1 -1
  221. package/dist/src/provider-device-runtimes.js +1 -0
  222. package/dist/src/provider-limrun-credentials.js +1 -0
  223. package/dist/src/provider-profile-fields.js +1 -0
  224. package/dist/src/provider-session-ownership.js +2 -2
  225. package/dist/src/provider-webdriver.js +2 -1
  226. package/dist/src/providers.js +1 -1
  227. package/dist/src/proxy.js +1 -1
  228. package/dist/src/quality-warnings.js +1 -1
  229. package/dist/src/react-native.js +1 -1
  230. package/dist/src/record-runtime.js +1 -1
  231. package/dist/src/record-trace.js +1 -1
  232. package/dist/src/recording.js +1 -1
  233. package/dist/src/rect-center.js +1 -1
  234. package/dist/src/register-builtins.js +1 -1
  235. package/dist/src/registry.js +39 -39
  236. package/dist/src/remote-config2.js +1 -1
  237. package/dist/src/replay-divergence.js +1 -1
  238. package/dist/src/replay.js +2 -2
  239. package/dist/src/reporting.js +5 -5
  240. package/dist/src/request-admission.js +1 -1
  241. package/dist/src/request-cancel.js +1 -1
  242. package/dist/src/request-cancellation.js +1 -1
  243. package/dist/src/request-runtime-binding.js +1 -1
  244. package/dist/src/result-serialization.js +1 -1
  245. package/dist/src/retry.js +1 -1
  246. package/dist/src/runner-artifact-manifest.js +1 -1
  247. package/dist/src/runner-cache-metadata.js +3 -3
  248. package/dist/src/runner-client.js +1 -1
  249. package/dist/src/runner-dev-tools-security.js +1 -1
  250. package/dist/src/runner-device-readiness.js +1 -1
  251. package/dist/src/runner-disposal.js +1 -1
  252. package/dist/src/runner-error-classification.js +2 -2
  253. package/dist/src/runner-exchange.js +1 -1
  254. package/dist/src/runner-operations-facade.js +1 -1
  255. package/dist/src/runner-session-types.js +1 -1
  256. package/dist/src/runner-start-budget.js +1 -1
  257. package/dist/src/runner.js +1 -1
  258. package/dist/src/runtime-admission.js +1 -1
  259. package/dist/src/runtime-transport-hints.js +1 -1
  260. package/dist/src/runtime.js +1 -1
  261. package/dist/src/runtime10.js +2 -2
  262. package/dist/src/runtime2.js +3 -3
  263. package/dist/src/runtime3.js +1 -1
  264. package/dist/src/runtime4.js +5 -5
  265. package/dist/src/runtime5.js +1 -1
  266. package/dist/src/runtime6.js +1 -1
  267. package/dist/src/runtime7.js +1 -1
  268. package/dist/src/runtime8.js +1 -1
  269. package/dist/src/runtime9.js +1 -1
  270. package/dist/src/screen-recording-admission-ledger.js +1 -1
  271. package/dist/src/screen-recording-completion.js +1 -1
  272. package/dist/src/screen-recording-options.js +1 -1
  273. package/dist/src/screen-recording-resource-recovery.js +1 -1
  274. package/dist/src/screen-recording-transport.js +1 -1
  275. package/dist/src/screenshot-density.js +1 -1
  276. package/dist/src/screenshot-image.js +1 -1
  277. package/dist/src/screenshot-result.js +1 -1
  278. package/dist/src/screenshot-runtime.js +1 -1
  279. package/dist/src/screenshot.js +1 -1
  280. package/dist/src/screenshot3.js +1 -1
  281. package/dist/src/script.js +1 -1
  282. package/dist/src/scroll-command.js +1 -1
  283. package/dist/src/scroll-gesture.js +1 -1
  284. package/dist/src/sdk-batch-runner.js +2 -2
  285. package/dist/src/sdk-batch.js +2 -2
  286. package/dist/src/sdk-contracts.d.ts +45 -2
  287. package/dist/src/sdk-contracts.js +2 -2
  288. package/dist/src/sdk-device.js +1 -1
  289. package/dist/src/sdk-io.js +1 -1
  290. package/dist/src/sdk-remote-config.js +1 -1
  291. package/dist/src/sdk-selectors.d.ts +33 -4
  292. package/dist/src/selection.js +0 -0
  293. package/dist/src/selector-runtime.js +1 -1
  294. package/dist/src/selectors.d.ts +2 -2
  295. package/dist/src/selectors.js +1 -1
  296. package/dist/src/server.js +1 -1
  297. package/dist/src/session-allocation.js +1 -1
  298. package/dist/src/session-artifact-paths.js +1 -1
  299. package/dist/src/session-repair-tombstone.js +1 -1
  300. package/dist/src/session-routing.js +1 -1
  301. package/dist/src/session-runtime-admission.js +1 -1
  302. package/dist/src/session-snapshot.js +1 -1
  303. package/dist/src/session-stream.js +27 -0
  304. package/dist/src/session.js +1 -1
  305. package/dist/src/session2.js +5 -5
  306. package/dist/src/settings.js +1 -1
  307. package/dist/src/settings2.js +1 -1
  308. package/dist/src/simctl-settings-facade.js +1 -0
  309. package/dist/src/simctl-settings.js +1 -0
  310. package/dist/src/simctl.js +1 -1
  311. package/dist/src/simulator.js +1 -1
  312. package/dist/src/snapshot-diagnostics.js +1 -1
  313. package/dist/src/snapshot-runtime-binding.js +1 -1
  314. package/dist/src/snapshot-runtime.js +1 -1
  315. package/dist/src/snapshot-state.js +1 -1
  316. package/dist/src/snapshot2.js +1 -1
  317. package/dist/src/snapshot3.js +1 -1
  318. package/dist/src/snapshot4.js +1 -1
  319. package/dist/src/src.js +1 -1
  320. package/dist/src/src7.js +1 -1
  321. package/dist/src/src8.js +2 -2
  322. package/dist/src/store.js +1 -0
  323. package/dist/src/string-enum.js +1 -1
  324. package/dist/src/surface-snapshot.js +1 -1
  325. package/dist/src/takeover.js +1 -1
  326. package/dist/src/target-evidence.js +1 -1
  327. package/dist/src/telemetry.js +1 -1
  328. package/dist/src/text-entry-focus.js +1 -1
  329. package/dist/src/tool-provider.js +1 -1
  330. package/dist/src/tool-provider2.js +1 -1
  331. package/dist/src/toolchain-probe.js +2 -2
  332. package/dist/src/touch-runtime.js +1 -1
  333. package/dist/src/touch-runtime2.js +1 -1
  334. package/dist/src/transport.js +2 -2
  335. package/dist/src/tv-remote.js +1 -1
  336. package/dist/src/type-text-runtime.js +1 -1
  337. package/dist/src/update-check.js +1 -1
  338. package/dist/src/vars.js +1 -1
  339. package/dist/src/verified-file.js +1 -1
  340. package/dist/src/version.js +1 -1
  341. package/dist/src/video.js +1 -1
  342. package/dist/src/viewport.js +1 -1
  343. package/dist/src/web.js +1 -1
  344. package/dist/src/web2.js +1 -1
  345. package/dist/src/webdriver-source.js +1 -1
  346. package/package.json +5 -1
  347. package/server.json +2 -2
  348. package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.20.apk.sha256 +0 -1
  349. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.20.apk +0 -0
  350. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.20.apk.sha256 +0 -1
  351. package/dist/src/api.js +0 -3
  352. package/dist/src/daemon-process.js +0 -1
  353. package/dist/src/multitouch-support.js +0 -1
  354. package/dist/src/parameterized-recorded-fill.js +0 -1
  355. package/dist/src/runner-startup-transport.js +0 -1
  356. package/dist/src/runner-xctestrun.js +0 -1
  357. package/dist/src/session-store.js +0 -4
@@ -0,0 +1 @@
1
+ c271cf2448728e2b10c51e132ea9393d1fdc4696578febcd361f6a6470b74b89 agent-device-android-ime-helper-0.21.22.apk
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "android-ime-helper",
3
- "version": "0.21.20",
4
- "assetName": "agent-device-android-ime-helper-0.21.20.apk",
5
- "sha256": "232a1d7a3c39d9acf586ea07bf6c17edc382313a568272f6bab2ef3458d2b420",
3
+ "version": "0.21.22",
4
+ "assetName": "agent-device-android-ime-helper-0.21.22.apk",
5
+ "sha256": "c271cf2448728e2b10c51e132ea9393d1fdc4696578febcd361f6a6470b74b89",
6
6
  "packageName": "com.callstack.agentdevice.imehelper",
7
- "versionCode": 21020,
7
+ "versionCode": 21022,
8
8
  "serviceComponent": "com.callstack.agentdevice.imehelper/.TestInputMethodService",
9
9
  "broadcastProtocol": "android-ime-helper-v1"
10
10
  }
@@ -0,0 +1 @@
1
+ e39e73088e371a3cee63b403f12609eea53acd37c810b0a1241e9c5285ac2c07 agent-device-android-snapshot-helper-0.21.22.apk
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "android-snapshot-helper",
3
- "version": "0.21.20",
4
- "releaseTag": "v0.21.20",
5
- "assetName": "agent-device-android-snapshot-helper-0.21.20.apk",
3
+ "version": "0.21.22",
4
+ "releaseTag": "v0.21.22",
5
+ "assetName": "agent-device-android-snapshot-helper-0.21.22.apk",
6
6
  "apkUrl": null,
7
- "sha256": "4fa070b457f9b178e7c05706ff95c79cbb171da08dd5bd28dbb375f6293efc12",
8
- "checksumName": "agent-device-android-snapshot-helper-0.21.20.apk.sha256",
7
+ "sha256": "e39e73088e371a3cee63b403f12609eea53acd37c810b0a1241e9c5285ac2c07",
8
+ "checksumName": "agent-device-android-snapshot-helper-0.21.22.apk.sha256",
9
9
  "packageName": "com.callstack.agentdevice.snapshothelper",
10
- "versionCode": 21020,
10
+ "versionCode": 21022,
11
11
  "instrumentationRunner": "com.callstack.agentdevice.snapshothelper/.SnapshotInstrumentation",
12
12
  "minSdk": 23,
13
13
  "targetSdk": 36,
@@ -0,0 +1,73 @@
1
+ import AppKit
2
+ import CoreGraphics
3
+ import Foundation
4
+ import ScreenCaptureKit
5
+
6
+ /// Captures the session app's front window by itself: windows above it, the ghost cursor, and
7
+ /// the rest of the desktop are not in the image, so the app may sit behind the user's work.
8
+ func captureAppWindowScreenshot(app: NSRunningApplication, outPath: String) throws {
9
+ guard #available(macOS 14.0, *) else {
10
+ throw HelperError.commandFailed("app window screenshots require macOS 14 or newer")
11
+ }
12
+ let pid = app.processIdentifier
13
+ let frontWindowID = frontWindowNumber(pid: pid)
14
+ let content = try awaitShareableContent()
15
+ let candidates = content.windows.filter {
16
+ $0.owningApplication?.processID == pid && $0.windowLayer == 0 && $0.frame.width > 0
17
+ && $0.frame.height > 0
18
+ }
19
+ guard
20
+ let window = candidates.first(where: { Int($0.windowID) == frontWindowID })
21
+ ?? candidates.max(by: { $0.frame.width * $0.frame.height < $1.frame.width * $1.frame.height })
22
+ else {
23
+ throw HelperError.commandFailed(
24
+ "screenshot could not find a window for the app",
25
+ details: ["reason": "window-not-found", "bundleId": app.bundleIdentifier ?? ""]
26
+ )
27
+ }
28
+
29
+ let filter = SCContentFilter(desktopIndependentWindow: window)
30
+ let configuration = SCStreamConfiguration()
31
+ let scale = CGFloat(filter.pointPixelScale)
32
+ configuration.width = Int(filter.contentRect.width * scale)
33
+ configuration.height = Int(filter.contentRect.height * scale)
34
+ configuration.showsCursor = false
35
+ configuration.ignoreShadowsSingleWindow = true
36
+
37
+ let semaphore = DispatchSemaphore(value: 0)
38
+ var capturedImage: CGImage?
39
+ var capturedError: Error?
40
+ SCScreenshotManager.captureImage(contentFilter: filter, configuration: configuration) { image, error in
41
+ capturedImage = image
42
+ capturedError = error
43
+ semaphore.signal()
44
+ }
45
+ semaphore.wait()
46
+ if let error = capturedError as NSError? {
47
+ throw screenshotFailure(error, surface: "app")
48
+ }
49
+ guard let capturedImage else {
50
+ throw HelperError.commandFailed("screenshot failed")
51
+ }
52
+ try writePNG(capturedImage, to: outPath)
53
+ }
54
+
55
+ @available(macOS 14.0, *)
56
+ private func awaitShareableContent() throws -> SCShareableContent {
57
+ let semaphore = DispatchSemaphore(value: 0)
58
+ var shareable: SCShareableContent?
59
+ var failure: Error?
60
+ SCShareableContent.getExcludingDesktopWindows(true, onScreenWindowsOnly: true) { content, error in
61
+ shareable = content
62
+ failure = error
63
+ semaphore.signal()
64
+ }
65
+ semaphore.wait()
66
+ if let error = failure as NSError? {
67
+ throw screenshotFailure(error, surface: "app")
68
+ }
69
+ guard let shareable else {
70
+ throw HelperError.commandFailed("screenshot could not list shareable windows")
71
+ }
72
+ return shareable
73
+ }
@@ -0,0 +1,557 @@
1
+ import AgentDeviceMacOSInput
2
+ import AppKit
3
+ import ApplicationServices
4
+ import CoreGraphics
5
+ import Foundation
6
+
7
+ /// How an app-surface action reached the app. Every pointer action is an accessibility action on
8
+ /// the element under the point: an inactive app drops pointer events posted to its process, so a
9
+ /// pointer action with no accessibility equivalent is refused rather than reported as delivered.
10
+ enum BackgroundDeliveryMechanism: String, Encodable, CaseIterable {
11
+ case axPress = "ax-press"
12
+ case axFocus = "ax-focus"
13
+ case axValue = "ax-value"
14
+ case axSelectedText = "ax-selected-text"
15
+ case axConfirm = "ax-confirm"
16
+ case axScrollBar = "ax-scroll-bar"
17
+ /// Keyboard events posted to the app's process, which inactive apps do accept.
18
+ case keyEvents = "key-events"
19
+ }
20
+
21
+ /// Typed refusals the host maps to `UNSUPPORTED_OPERATION`. Both vocabularies are pinned by
22
+ /// `contracts/fixtures/macos-native-helper-outcomes.json`.
23
+ enum BackgroundRefusal: String, CaseIterable {
24
+ case pointerGesture = "background-pointer-gesture"
25
+ case noAccessibleTarget = "no-accessible-target"
26
+ case noTextInput = "no-settable-text-input"
27
+ case noScrollBar = "no-scroll-bar"
28
+ }
29
+
30
+ struct BackgroundPressResponse: Encodable {
31
+ let x: Double
32
+ let y: Double
33
+ let clicks: Int
34
+ let bundleId: String?
35
+ let surface: String
36
+ let mechanism: BackgroundDeliveryMechanism
37
+ let role: String?
38
+ let windowTitle: String?
39
+ }
40
+
41
+ struct BackgroundTextResponse: Encodable {
42
+ let bundleId: String?
43
+ let mechanism: BackgroundDeliveryMechanism
44
+ let role: String?
45
+ let windowTitle: String?
46
+ }
47
+
48
+ struct BackgroundScrollResponse: Encodable {
49
+ let x: Double
50
+ let y: Double
51
+ let x2: Double
52
+ let y2: Double
53
+ let referenceWidth: Double
54
+ let referenceHeight: Double
55
+ let travelPixels: Double
56
+ let mechanism: BackgroundDeliveryMechanism
57
+ }
58
+
59
+ /// Roles whose ancestors are containers, never the control a click meant.
60
+ private let pressSearchBoundaryRoles: Set<String> = [
61
+ "AXWindow", "AXApplication", "AXScrollArea", "AXTable", "AXOutline", "AXList", "AXWebArea",
62
+ "AXSheet", "AXDialog",
63
+ ]
64
+ private let textInputRoles: Set<String> = ["AXTextField", "AXTextArea", "AXComboBox", "AXSearchField"]
65
+ /// Roles a click means to activate. Chromium marks most wrapper groups pressable too, so a group
66
+ /// is only the target when nothing more specific contains the point.
67
+ private let pressableControlRoles: Set<String> = [
68
+ "AXButton", "AXLink", "AXCheckBox", "AXRadioButton", "AXPopUpButton", "AXMenuButton",
69
+ "AXMenuItem", "AXMenuBarItem", "AXCell", "AXRow", "AXTab", "AXDisclosureTriangle",
70
+ "AXIncrementor", "AXSlider", "AXColorWell", "AXDockItem",
71
+ ]
72
+ private let pressSearchMaxAncestors = 4
73
+
74
+ func refusal(_ reason: BackgroundRefusal, _ message: String, app: NSRunningApplication) -> HelperError {
75
+ .commandFailed(message, details: ["reason": reason.rawValue, "bundleId": app.bundleIdentifier ?? ""])
76
+ }
77
+
78
+ func requireSessionApplication(bundleId: String?) throws -> NSRunningApplication {
79
+ guard let bundleId, !bundleId.isEmpty else {
80
+ throw HelperError.invalidArgs("the app surface requires --bundle-id <id>")
81
+ }
82
+ return try resolveTargetApplication(bundleId: bundleId, surface: "app")
83
+ }
84
+
85
+ func pressInBackground(
86
+ _ request: MouseClickRequest,
87
+ app: NSRunningApplication,
88
+ cursor: GhostCursor?
89
+ ) throws -> BackgroundPressResponse {
90
+ guard !request.doubleClick, request.holdMs <= 0 else {
91
+ throw refusal(
92
+ .pointerGesture,
93
+ "double-click and press-and-hold have no accessibility equivalent in a background app",
94
+ app: app
95
+ )
96
+ }
97
+ let point = CGPoint(x: request.x, y: request.y)
98
+ try requireAppWindowPoint(point, app: app)
99
+ cursor?.move(to: point)
100
+ let target = resolvePressTarget(app: app, point: point)
101
+ if let target, isInMenuBar(target.element) {
102
+ throw refusal(.noAccessibleTarget, "the point is on the app's menu bar", app: app)
103
+ }
104
+ guard let target, let mechanism = perform(target) else {
105
+ throw refusal(.noAccessibleTarget, "no pressable accessibility element at the point", app: app)
106
+ }
107
+ var clicks = 1
108
+ while clicks < request.clicks {
109
+ Thread.sleep(forTimeInterval: Double(max(request.intervalMs, 0)) / 1000)
110
+ guard perform(target) != nil else { break }
111
+ clicks += 1
112
+ }
113
+ cursor?.pulse()
114
+ return BackgroundPressResponse(
115
+ x: request.x,
116
+ y: request.y,
117
+ clicks: clicks,
118
+ bundleId: app.bundleIdentifier,
119
+ surface: "app",
120
+ mechanism: mechanism,
121
+ role: role(of: target.element),
122
+ windowTitle: windowTitle(of: target.element)
123
+ )
124
+ }
125
+
126
+ /// Inserts text at the focused element's caret, the way typing does.
127
+ func typeInBackground(
128
+ text: String,
129
+ delayMs: Int,
130
+ app: NSRunningApplication,
131
+ cursor: GhostCursor?
132
+ ) throws -> BackgroundTextResponse {
133
+ let appElement = AXUIElementCreateApplication(app.processIdentifier)
134
+ let focused = elementAttribute(appElement, attribute: kAXFocusedUIElementAttribute as String)
135
+ if let focused, let rect = rectAttribute(focused) {
136
+ cursor?.move(to: CGPoint(x: rect.x + rect.width / 2, y: rect.y + rect.height / 2))
137
+ }
138
+ let focusedRole = focused.map(role(of:))
139
+ if text == "\n", let focused, actionNames(of: focused).contains(kAXConfirmAction as String),
140
+ AXUIElementPerformAction(focused, kAXConfirmAction as CFString) == .success
141
+ {
142
+ cursor?.pulse()
143
+ return BackgroundTextResponse(
144
+ bundleId: app.bundleIdentifier, mechanism: .axConfirm, role: focusedRole,
145
+ windowTitle: windowTitle(of: focused))
146
+ }
147
+ if !text.contains("\n"), let focused,
148
+ isAttributeSettable(focused, attribute: kAXSelectedTextAttribute as String),
149
+ AXUIElementSetAttributeValue(focused, kAXSelectedTextAttribute as CFString, text as CFString)
150
+ == .success
151
+ {
152
+ cursor?.pulse()
153
+ return BackgroundTextResponse(
154
+ bundleId: app.bundleIdentifier, mechanism: .axSelectedText, role: focusedRole,
155
+ windowTitle: windowTitle(of: focused))
156
+ }
157
+ try postText(text, delayMs: delayMs, pid: app.processIdentifier)
158
+ cursor?.pulse()
159
+ return BackgroundTextResponse(
160
+ bundleId: app.bundleIdentifier, mechanism: .keyEvents, role: focusedRole,
161
+ windowTitle: focused.flatMap(windowTitle(of:)))
162
+ }
163
+
164
+ /// Replaces the value of the text input at a point.
165
+ func fillInBackground(
166
+ point: CGPoint,
167
+ text: String,
168
+ app: NSRunningApplication,
169
+ cursor: GhostCursor?
170
+ ) throws -> BackgroundTextResponse {
171
+ try requireAppWindowPoint(point, app: app)
172
+ cursor?.move(to: point)
173
+ let hit = elementAtPoint(in: app, point: point)
174
+ let chain = hit.map(pressSearchChain) ?? []
175
+ let fallback = actionWindow(app: app, hit: hit).flatMap { window in
176
+ smallestElement(in: window, containing: point, where: isTextInput)
177
+ }
178
+ if let input = chain.first(where: isTextInput) ?? fallback, isInMenuBar(input) {
179
+ throw refusal(.noAccessibleTarget, "the point is on the app's menu bar", app: app)
180
+ }
181
+ guard let input = chain.first(where: isTextInput) ?? fallback,
182
+ isAttributeSettable(input, attribute: kAXValueAttribute as String)
183
+ else {
184
+ throw refusal(.noTextInput, "no settable text input at the point", app: app)
185
+ }
186
+ AXUIElementSetAttributeValue(input, kAXFocusedAttribute as CFString, kCFBooleanTrue)
187
+ guard AXUIElementSetAttributeValue(input, kAXValueAttribute as CFString, text as CFString) == .success
188
+ else {
189
+ throw refusal(.noTextInput, "the text input refused the new value", app: app)
190
+ }
191
+ cursor?.pulse()
192
+ return BackgroundTextResponse(
193
+ bundleId: app.bundleIdentifier, mechanism: .axValue, role: role(of: input),
194
+ windowTitle: windowTitle(of: input))
195
+ }
196
+
197
+ /// Scrolls the scroll area at the center of the app's front window.
198
+ func scrollInBackground(
199
+ direction: String,
200
+ amount: Double?,
201
+ pixels: Double?,
202
+ app: NSRunningApplication,
203
+ cursor: GhostCursor?
204
+ ) throws -> BackgroundScrollResponse {
205
+ let isVertical = direction == "up" || direction == "down"
206
+ guard isVertical || direction == "left" || direction == "right" else {
207
+ throw HelperError.invalidArgs("scroll requires --direction <up|down|left|right>")
208
+ }
209
+ guard let frame = frontWindowFrame(pid: app.processIdentifier) else {
210
+ throw HelperError.commandFailed(
211
+ "scroll could not resolve an on-screen window",
212
+ details: ["reason": "window-not-found", "bundleId": app.bundleIdentifier ?? ""]
213
+ )
214
+ }
215
+ let travel = scrollTravelPixels(
216
+ axisLength: Double(isVertical ? frame.height : frame.width),
217
+ amount: amount,
218
+ pixels: pixels
219
+ )
220
+ let center = CGPoint(x: frame.midX, y: frame.midY)
221
+ cursor?.move(to: center)
222
+ let revealsLaterContent = direction == "down" || direction == "right"
223
+ guard
224
+ performScrollBarScroll(
225
+ app: app,
226
+ at: center,
227
+ isVertical: isVertical,
228
+ signedTravel: revealsLaterContent ? travel : -travel
229
+ )
230
+ else {
231
+ throw refusal(.noScrollBar, "no scroll area with a settable scroll bar under the window center", app: app)
232
+ }
233
+ cursor?.pulse()
234
+ // Reported as the equivalent drag, start to end, the way the runner reports a desktop scroll.
235
+ let half = travel / 2
236
+ let sign: Double = revealsLaterContent ? 1 : -1
237
+ return BackgroundScrollResponse(
238
+ x: isVertical ? center.x : center.x + sign * half,
239
+ y: isVertical ? center.y + sign * half : center.y,
240
+ x2: isVertical ? center.x : center.x - sign * half,
241
+ y2: isVertical ? center.y - sign * half : center.y,
242
+ referenceWidth: Double(frame.width),
243
+ referenceHeight: Double(frame.height),
244
+ travelPixels: travel,
245
+ mechanism: .axScrollBar
246
+ )
247
+ }
248
+
249
+ /// Mirrors `runnerScrollGesturePlan`'s travel so both macOS backends scroll the same distance:
250
+ /// the requested travel, kept clear of the outer tenth of the axis on both sides.
251
+ func scrollTravelPixels(axisLength: Double, amount: Double?, pixels: Double?) -> Double {
252
+ let requested = pixels.map { max(1, $0.rounded()) } ?? (axisLength * (amount ?? 0.6)).rounded()
253
+ let edgePadding = max(1, (axisLength * 0.1).rounded())
254
+ return max(1, min(requested, axisLength - edgePadding * 2))
255
+ }
256
+
257
+ /// The scroll bar value that moves the content by `signedTravel` points, clamped to its ends.
258
+ func scrollBarValue(current: Double, signedTravel: Double, overflow: Double) -> Double {
259
+ min(1, max(0, current + signedTravel / overflow))
260
+ }
261
+
262
+ func isPressableControlRole(_ role: String) -> Bool {
263
+ pressableControlRoles.contains(role)
264
+ }
265
+
266
+ private enum PressTarget {
267
+ case focus(AXUIElement)
268
+ case press(AXUIElement)
269
+
270
+ var element: AXUIElement {
271
+ switch self {
272
+ case .focus(let element), .press(let element): element
273
+ }
274
+ }
275
+ }
276
+
277
+ /// What a click at a point means. The app's own hit test answers first; web content often answers
278
+ /// with a wrapper group, so the snapshot geometry decides when the hit names no control.
279
+ private func resolvePressTarget(app: NSRunningApplication, point: CGPoint) -> PressTarget? {
280
+ let hit = elementAtPoint(in: app, point: point)
281
+ let chain = hit.map(pressSearchChain) ?? []
282
+ if let input = chain.first(where: isTextInput) { return .focus(input) }
283
+ if let control = chain.first(where: isPressableControl) { return .press(control) }
284
+ if let found = actionWindow(app: app, hit: hit).flatMap({
285
+ smallestElement(in: $0, containing: point, where: { isTextInput($0) || isPressableControl($0) })
286
+ }) {
287
+ return isTextInput(found) ? .focus(found) : .press(found)
288
+ }
289
+ return chain.first(where: { actionNames(of: $0).contains(kAXPressAction as String) }).map {
290
+ .press($0)
291
+ }
292
+ }
293
+
294
+ private func perform(_ target: PressTarget) -> BackgroundDeliveryMechanism? {
295
+ switch target {
296
+ case .focus(let element):
297
+ return AXUIElementSetAttributeValue(element, kAXFocusedAttribute as CFString, kCFBooleanTrue)
298
+ == .success ? .axFocus : nil
299
+ case .press(let element):
300
+ return AXUIElementPerformAction(element, kAXPressAction as CFString) == .success ? .axPress : nil
301
+ }
302
+ }
303
+
304
+ /// The app's accessibility tree also holds its menu bar, whose Apple menu acts for the whole Mac
305
+ /// (Sleep, Lock Screen, Recent Items). A point action therefore lands only inside one of the app's
306
+ /// own on-screen windows, its menus and sheets included, and never on a menu bar element: the app
307
+ /// owns its menu bar's window too, so the window check alone does not exclude it.
308
+ private func requireAppWindowPoint(_ point: CGPoint, app: NSRunningApplication) throws {
309
+ guard
310
+ let info = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID)
311
+ as? [[String: Any]]
312
+ else {
313
+ throw refusal(.noAccessibleTarget, "the app's on-screen windows could not be read", app: app)
314
+ }
315
+ let inAppWindow = info.contains { entry in
316
+ guard (entry[kCGWindowOwnerPID as String] as? Int32) == app.processIdentifier,
317
+ let boundsDict = entry[kCGWindowBounds as String] as? NSDictionary,
318
+ let bounds = CGRect(dictionaryRepresentation: boundsDict as CFDictionary)
319
+ else {
320
+ return false
321
+ }
322
+ return bounds.contains(point)
323
+ }
324
+ guard inAppWindow else {
325
+ throw refusal(.noAccessibleTarget, "the point is outside the app's on-screen windows", app: app)
326
+ }
327
+ }
328
+
329
+ private func isInMenuBar(_ element: AXUIElement) -> Bool {
330
+ var current: AXUIElement? = element
331
+ var depth = 0
332
+ while let node = current, depth < 32 {
333
+ let nodeRole = role(of: node)
334
+ if nodeRole == "AXMenuBar" || nodeRole == "AXMenuBarItem" { return true }
335
+ if nodeRole == "AXApplication" { return false }
336
+ current = elementAttribute(node, attribute: kAXParentAttribute as String)
337
+ depth += 1
338
+ }
339
+ return false
340
+ }
341
+
342
+ /// The app's own hit test, scoped to the app so windows of other apps above it do not answer.
343
+ private func elementAtPoint(in app: NSRunningApplication, point: CGPoint) -> AXUIElement? {
344
+ let appElement = AXUIElementCreateApplication(app.processIdentifier)
345
+ var hit: AXUIElement?
346
+ guard AXUIElementCopyElementAtPosition(appElement, Float(point.x), Float(point.y), &hit) == .success
347
+ else {
348
+ return nil
349
+ }
350
+ return hit
351
+ }
352
+
353
+ private func pressSearchChain(from hit: AXUIElement) -> [AXUIElement] {
354
+ var chain: [AXUIElement] = []
355
+ var current: AXUIElement? = hit
356
+ while let element = current, chain.count <= pressSearchMaxAncestors {
357
+ if pressSearchBoundaryRoles.contains(role(of: element)) { break }
358
+ chain.append(element)
359
+ current = elementAttribute(element, attribute: kAXParentAttribute as String)
360
+ }
361
+ return chain
362
+ }
363
+
364
+ /// The one window a point action may act in: the window the app's hit test landed in, else the
365
+ /// app's front on-screen window. A fallback target never comes from a window behind it, and the
366
+ /// response names the window so the host can check it.
367
+ private func actionWindow(app: NSRunningApplication, hit: AXUIElement?) -> AXUIElement? {
368
+ if let hit {
369
+ if role(of: hit) == "AXWindow" { return hit }
370
+ if let window = elementAttribute(hit, attribute: kAXWindowAttribute as String) { return window }
371
+ }
372
+ guard let front = frontWindowFrame(pid: app.processIdentifier) else { return nil }
373
+ return windows(of: AXUIElementCreateApplication(app.processIdentifier)).first { window in
374
+ guard let rect = rectAttribute(window) else { return false }
375
+ return abs(rect.x - front.minX) < 1 && abs(rect.y - front.minY) < 1
376
+ && abs(rect.width - front.width) < 1 && abs(rect.height - front.height) < 1
377
+ }
378
+ }
379
+
380
+ private func windowTitle(of element: AXUIElement) -> String? {
381
+ let window = role(of: element) == "AXWindow"
382
+ ? element : elementAttribute(element, attribute: kAXWindowAttribute as String)
383
+ return window.flatMap { stringAttribute($0, attribute: kAXTitleAttribute as String) }
384
+ }
385
+
386
+ /// The smallest matching element in a window whose frame contains the point, skipping subtrees
387
+ /// whose frame excludes the point.
388
+ private func smallestElement(
389
+ in window: AXUIElement,
390
+ containing point: CGPoint,
391
+ where matches: (AXUIElement) -> Bool
392
+ ) -> AXUIElement? {
393
+ var best: (element: AXUIElement, area: Double)?
394
+ var budget = 6000
395
+ func visit(_ element: AXUIElement, depth: Int) {
396
+ guard budget > 0, depth < 64 else { return }
397
+ budget -= 1
398
+ if let rect = rectAttribute(element), rect.width > 0, rect.height > 0 {
399
+ guard CGRect(x: rect.x, y: rect.y, width: rect.width, height: rect.height).contains(point)
400
+ else { return }
401
+ let area = rect.width * rect.height
402
+ if area < (best?.area ?? .infinity), matches(element) {
403
+ best = (element, area)
404
+ }
405
+ }
406
+ for child in children(of: element) {
407
+ visit(child, depth: depth + 1)
408
+ }
409
+ }
410
+ visit(window, depth: 0)
411
+ return best?.element
412
+ }
413
+
414
+ /// Moves the scroll bar of the scroll area under a point by the travel's share of the content's
415
+ /// overflow. An inactive app honors a scroll bar change where it drops wheel events.
416
+ private func performScrollBarScroll(
417
+ app: NSRunningApplication,
418
+ at point: CGPoint,
419
+ isVertical: Bool,
420
+ signedTravel: Double
421
+ ) -> Bool {
422
+ var current = elementAtPoint(in: app, point: point)
423
+ for _ in 0..<8 {
424
+ guard let element = current, role(of: element) != "AXWindow" else { return false }
425
+ current = elementAttribute(element, attribute: kAXParentAttribute as String)
426
+ guard role(of: element) == "AXScrollArea" else { continue }
427
+ let barAttribute = isVertical ? kAXVerticalScrollBarAttribute : kAXHorizontalScrollBarAttribute
428
+ guard let bar = elementAttribute(element, attribute: barAttribute as String),
429
+ isAttributeSettable(bar, attribute: kAXValueAttribute as String),
430
+ let value = numberAttribute(bar, attribute: kAXValueAttribute as String),
431
+ let area = rectAttribute(element),
432
+ let content = children(of: element)
433
+ .filter({ role(of: $0) != "AXScrollBar" })
434
+ .compactMap(rectAttribute)
435
+ .max(by: { $0.width * $0.height < $1.width * $1.height })
436
+ else {
437
+ continue
438
+ }
439
+ let overflow = isVertical ? content.height - area.height : content.width - area.width
440
+ guard overflow > 0 else { continue }
441
+ let next = scrollBarValue(current: value, signedTravel: signedTravel, overflow: overflow)
442
+ return AXUIElementSetAttributeValue(bar, kAXValueAttribute as CFString, NSNumber(value: next))
443
+ == .success
444
+ }
445
+ return false
446
+ }
447
+
448
+ private func role(of element: AXUIElement) -> String {
449
+ stringAttribute(element, attribute: kAXRoleAttribute as String) ?? ""
450
+ }
451
+
452
+ private func isTextInput(_ element: AXUIElement) -> Bool {
453
+ textInputRoles.contains(role(of: element))
454
+ }
455
+
456
+ private func isPressableControl(_ element: AXUIElement) -> Bool {
457
+ isPressableControlRole(role(of: element))
458
+ && actionNames(of: element).contains(kAXPressAction as String)
459
+ }
460
+
461
+ private func actionNames(of element: AXUIElement) -> [String] {
462
+ var names: CFArray?
463
+ guard AXUIElementCopyActionNames(element, &names) == .success, let names = names as? [String] else {
464
+ return []
465
+ }
466
+ return names
467
+ }
468
+
469
+ private func isAttributeSettable(_ element: AXUIElement, attribute: String) -> Bool {
470
+ var settable = DarwinBoolean(false)
471
+ return AXUIElementIsAttributeSettable(element, attribute as CFString, &settable) == .success
472
+ && settable.boolValue
473
+ }
474
+
475
+ private func numberAttribute(_ element: AXUIElement, attribute: String) -> Double? {
476
+ var value: CFTypeRef?
477
+ guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
478
+ let number = value as? NSNumber
479
+ else {
480
+ return nil
481
+ }
482
+ return number.doubleValue
483
+ }
484
+
485
+ private struct OnScreenWindow {
486
+ let number: Int
487
+ let bounds: CGRect
488
+ }
489
+
490
+ /// The pid's normal-layer windows, front to back. Bounds and owners need no Screen Recording
491
+ /// permission; only titles do.
492
+ private func onScreenWindows(pid: pid_t) -> [OnScreenWindow] {
493
+ guard
494
+ let info = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID)
495
+ as? [[String: Any]]
496
+ else {
497
+ return []
498
+ }
499
+ return info.compactMap { entry in
500
+ guard (entry[kCGWindowOwnerPID as String] as? Int32) == pid,
501
+ (entry[kCGWindowLayer as String] as? Int) == 0,
502
+ let number = entry[kCGWindowNumber as String] as? Int,
503
+ let boundsDict = entry[kCGWindowBounds as String] as? NSDictionary,
504
+ let bounds = CGRect(dictionaryRepresentation: boundsDict as CFDictionary)
505
+ else {
506
+ return nil
507
+ }
508
+ return OnScreenWindow(number: number, bounds: bounds)
509
+ }
510
+ }
511
+
512
+ func frontWindowNumber(pid: pid_t) -> Int? {
513
+ onScreenWindows(pid: pid).first?.number
514
+ }
515
+
516
+ private func frontWindowFrame(pid: pid_t) -> CGRect? {
517
+ onScreenWindows(pid: pid).first?.bounds
518
+ }
519
+
520
+ private let returnVirtualKey: CGKeyCode = 36
521
+ private let tabVirtualKey: CGKeyCode = 48
522
+
523
+ private func postKey(virtualKey: CGKeyCode, pid: pid_t) throws {
524
+ guard let down = CGEvent(keyboardEventSource: nil, virtualKey: virtualKey, keyDown: true),
525
+ let up = CGEvent(keyboardEventSource: nil, virtualKey: virtualKey, keyDown: false)
526
+ else {
527
+ throw HelperError.commandFailed("key event creation failed", details: ["reason": "event_creation_failed"])
528
+ }
529
+ down.postToPid(pid)
530
+ up.postToPid(pid)
531
+ }
532
+
533
+ private func postText(_ text: String, delayMs: Int, pid: pid_t) throws {
534
+ for character in text {
535
+ switch character {
536
+ case "\n", "\r":
537
+ try postKey(virtualKey: returnVirtualKey, pid: pid)
538
+ case "\t":
539
+ try postKey(virtualKey: tabVirtualKey, pid: pid)
540
+ default:
541
+ guard let down = CGEvent(keyboardEventSource: nil, virtualKey: 0, keyDown: true),
542
+ let up = CGEvent(keyboardEventSource: nil, virtualKey: 0, keyDown: false)
543
+ else {
544
+ throw HelperError.commandFailed(
545
+ "key event creation failed", details: ["reason": "event_creation_failed"])
546
+ }
547
+ let units = Array(String(character).utf16)
548
+ down.keyboardSetUnicodeString(stringLength: units.count, unicodeString: units)
549
+ up.keyboardSetUnicodeString(stringLength: units.count, unicodeString: units)
550
+ down.postToPid(pid)
551
+ up.postToPid(pid)
552
+ }
553
+ if delayMs > 0 {
554
+ Thread.sleep(forTimeInterval: Double(delayMs) / 1000)
555
+ }
556
+ }
557
+ }