agent-device 0.21.19 → 0.21.21
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.
- package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.19.apk → agent-device-android-ime-helper-0.21.21.apk} +0 -0
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.21.apk.sha256 +1 -0
- package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.19.manifest.json → agent-device-android-ime-helper-0.21.21.manifest.json} +4 -4
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.21.apk +0 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.21.apk.sha256 +1 -0
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.19.manifest.json → agent-device-android-snapshot-helper-0.21.21.manifest.json} +6 -6
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/AppWindowScreenshot.swift +73 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/BackgroundInteraction.swift +557 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/GhostCursor.swift +141 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift +107 -3
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/main.swift +136 -22
- package/apple/macos-helper/Tests/AgentDeviceMacOSHelperTests/BackgroundInteractionTests.swift +88 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +133 -4
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +10 -2
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +36 -9
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +28 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +1 -14
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +14 -16
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +67 -14
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedCommitDeadline.swift +6 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +7 -2
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedInteraction.swift +21 -7
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedTextEntry.swift +121 -17
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +179 -6
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryConfirmation.swift +119 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryFocus.swift +8 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryReadiness.swift +23 -11
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextInputProbe.swift +59 -6
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextTyping.swift +112 -23
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TypeExecution.swift +25 -12
- package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/RecordingExportSupport.swift +6 -1
- package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +43 -13
- package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotModels.swift +20 -5
- package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotPresentationProjection.swift +2 -15
- package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotVisibilityFold.swift +10 -25
- package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotVisibilityFoldProjection.swift +1 -18
- package/dist/src/adapter.js +1 -1
- package/dist/src/adb-executor.js +2 -2
- package/dist/src/adb-failure.js +1 -1
- package/dist/src/adb-transport.js +1 -1
- package/dist/src/agent-browser-lifecycle.js +1 -1
- package/dist/src/agent-browser-process-record.js +1 -1
- package/dist/src/agent-browser-provider.js +1 -1
- package/dist/src/agent-browser-tool.js +1 -1
- package/dist/src/agent-device-client.js +2 -2
- package/dist/src/ai-sdk.js +1 -1
- package/dist/src/alert-runtime.js +1 -1
- package/dist/src/alert.js +1 -0
- package/dist/src/android-adb.d.ts +12 -0
- package/dist/src/android-input-ownership.js +1 -1
- package/dist/src/app-backend.js +1 -0
- package/dist/src/app-catalog.js +1 -1
- package/dist/src/app-deployment-resolution.js +1 -1
- package/dist/src/app-inventory.js +1 -1
- package/dist/src/app-launch.js +2 -2
- package/dist/src/app-lifecycle.js +2 -2
- package/dist/src/app-lifecycle2.js +1 -1
- package/dist/src/app-log-files.js +1 -1
- package/dist/src/app-log-reconnect.js +1 -1
- package/dist/src/app-log-runtime2.js +3 -3
- package/dist/src/app-log-session-resource.js +1 -0
- package/dist/src/app-resolution.js +2 -2
- package/dist/src/apple-runner-platform.js +1 -1
- package/dist/src/application-lifecycle-interaction.js +1 -1
- package/dist/src/archive.js +2 -2
- package/dist/src/artifact-download.js +1 -1
- package/dist/src/auth-session.js +1 -1
- package/dist/src/auth.js +1 -1
- package/dist/src/back-mode.js +1 -1
- package/dist/src/backend-snapshot-options.js +1 -1
- package/dist/src/boot-diagnostics.js +1 -1
- package/dist/src/browser-launch.js +1 -1
- package/dist/src/cache.js +1 -1
- package/dist/src/capabilities.js +1 -1
- package/dist/src/capture.js +1 -1
- package/dist/src/cli-config.js +1 -1
- package/dist/src/cli-help.js +317 -323
- package/dist/src/cli-runner.js +1 -1
- package/dist/src/cli.js +4 -4
- package/dist/src/click-button.js +1 -1
- package/dist/src/client-metro-companion.js +1 -1
- package/dist/src/client-metro.js +1 -1
- package/dist/src/client-options.js +3 -0
- package/dist/src/client-types.d.ts +101 -6
- package/dist/src/clipboard-shell-response.js +1 -1
- package/dist/src/{scroll-command.d.ts → cloud-artifacts.d.ts} +249 -240
- package/dist/src/code-signature-cache.js +1 -1
- package/dist/src/code-signature.js +1 -1
- package/dist/src/command-schema.js +3 -3
- package/dist/src/command-tools.js +2 -2
- package/dist/src/command.js +1 -1
- package/dist/src/config.js +1 -1
- package/dist/src/connection-runtime.js +1 -1
- package/dist/src/connection.js +5 -5
- package/dist/src/contracts.d.ts +1 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/daemon-artifacts.js +1 -0
- package/dist/src/daemon-client-lease-beat.js +1 -1
- package/dist/src/daemon-client-lifecycle.js +1 -1
- package/dist/src/daemon-client-liveness-probe.js +1 -0
- package/dist/src/daemon-client-transport.js +1 -1
- package/dist/src/daemon-http.js +1 -0
- package/dist/src/daemon-policy-file.js +1 -1
- package/dist/src/daemon-registration-owner.js +1 -0
- package/dist/src/daemon-registration.js +1 -1
- package/dist/src/daemon-resolution.js +1 -1
- package/dist/src/daemon-shutdown-report.js +1 -1
- package/dist/src/daemon.js +2 -2
- package/dist/src/debug-symbols-facade.js +3 -3
- package/dist/src/device-claim-inspection.js +1 -1
- package/dist/src/device-claim-rule.js +1 -1
- package/dist/src/device-inventory-context.js +1 -1
- package/dist/src/device-readiness-runtime.js +1 -1
- package/dist/src/device-release.js +1 -1
- package/dist/src/device-rotation.js +1 -1
- package/dist/src/device-selection-resolver.js +1 -1
- package/dist/src/device-session.js +1 -1
- package/dist/src/device-shell.js +1 -1
- package/dist/src/device.js +1 -1
- package/dist/src/device2.js +1 -3
- package/dist/src/device3.js +3 -0
- package/dist/src/devicectl.js +1 -1
- package/dist/src/diagnostics.js +3 -3
- package/dist/src/dispatch-resolve.js +4 -1
- package/dist/src/display-inventory.js +1 -1
- package/dist/src/doctor-facade.js +1 -1
- package/dist/src/doctor2.js +1 -1
- package/dist/src/durable-capture-resource.js +1 -0
- package/dist/src/engine-eval-script.js +1 -1
- package/dist/src/error.js +1 -1
- package/dist/src/exec.js +1 -1
- package/dist/src/file.js +1 -1
- package/dist/src/find.js +1 -1
- package/dist/src/generic-settle.js +1 -1
- package/dist/src/generic.js +1 -1
- package/dist/src/gesture-admission.js +1 -1
- package/dist/src/gesture-plan.js +1 -1
- package/dist/src/gesture-runtime.js +1 -1
- package/dist/src/harmonyos.js +1 -1
- package/dist/src/hdc.js +1 -1
- package/dist/src/helper.js +2 -2
- package/dist/src/host-file.js +1 -1
- package/dist/src/host.js +1 -1
- package/dist/src/human-control-contract.js +1 -1
- package/dist/src/human-control.js +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/input-actions.js +1 -1
- package/dist/src/input-actions2.js +2 -2
- package/dist/src/install-artifact-facade.js +1 -1
- package/dist/src/install-source-network-transport.js +1 -1
- package/dist/src/install-source.d.ts +5 -0
- package/dist/src/install-source2.js +1 -1
- package/dist/src/instance-client.js +1 -0
- package/dist/src/interaction-positionals.js +1 -1
- package/dist/src/interaction-ref-policy.js +1 -1
- package/dist/src/interaction.js +1 -1
- package/dist/src/interaction2.js +1 -1
- package/dist/src/interactor-operation-binding.js +1 -1
- package/dist/src/interactor.js +5 -5
- package/dist/src/interactor2.js +1 -1
- package/dist/src/interactors.js +1 -1
- package/dist/src/internal/bin.js +1 -1
- package/dist/src/internal/daemon.js +1 -2
- package/dist/src/internal/png-worker.js +1 -1
- package/dist/src/internal/run-script-http-child.js +1 -1
- package/dist/src/inventory.js +4 -4
- package/dist/src/inventory2.js +1 -1
- package/dist/src/inventory3.js +1 -1
- package/dist/src/inventory5.js +1 -1
- package/dist/src/ios-client.js +4 -30
- package/dist/src/ios-settings.js +1 -0
- package/dist/src/ios-snapshot-engine.js +1 -1
- package/dist/src/ios-snapshot-runtime.js +1 -1
- package/dist/src/launch-confirmation.js +1 -0
- package/dist/src/lease-context.js +1 -1
- package/dist/src/lease-registry-scope.js +1 -0
- package/dist/src/lease-scope.js +1 -1
- package/dist/src/lease.js +1 -1
- package/dist/src/legacy-xctest-device-set.js +1 -1
- package/dist/src/limrun-download-file.js +1 -1
- package/dist/src/limrun-runtime-dependencies.js +1 -1
- package/dist/src/limrun.d.ts +78 -44
- package/dist/src/limrun.js +1 -1
- package/dist/src/linux.js +1 -1
- package/dist/src/load.js +1 -0
- package/dist/src/location-coordinates.js +1 -1
- package/dist/src/macos-facade.js +1 -1
- package/dist/src/managed-device-scope.js +1 -1
- package/dist/src/mechanics.js +16 -16
- package/dist/src/observation.js +1 -1
- package/dist/src/observe-until.js +1 -1
- package/dist/src/overlay.js +2 -2
- package/dist/src/owner-identity.js +3 -3
- package/dist/src/path-resolution.js +1 -1
- package/dist/src/perf-facade.js +1 -1
- package/dist/src/perf-runtime-operation-builder.js +1 -1
- package/dist/src/perf-runtime-plan.js +1 -1
- package/dist/src/perf-target.js +1 -1
- package/dist/src/perf.js +1 -1
- package/dist/src/physical-device-control.js +1 -1
- package/dist/src/platform-request-scope.js +1 -1
- package/dist/src/platform-runtime-android-adb-host.js +1 -1
- package/dist/src/platform-runtime-app-log-process.js +1 -1
- package/dist/src/platform-runtime-host.js +1 -1
- package/dist/src/platform-runtime-managed-owner.js +1 -1
- package/dist/src/platform-runtime-open-target.js +1 -1
- package/dist/src/platform-runtime-operation-host.js +2 -2
- package/dist/src/platform-runtime-runtime-hints.js +2 -2
- package/dist/src/platform-runtime-screen-recording-android-host.js +1 -1
- package/dist/src/platform-runtime-screen-recording-apple-runner-transport.js +1 -1
- package/dist/src/platform-runtime-screen-recording-apple-simulator-host.js +1 -1
- package/dist/src/platform-runtime-screen-recording-finalizer-host.js +1 -1
- package/dist/src/platform-runtime-screen-recording-output-host.js +1 -1
- package/dist/src/platform-runtime-screen-recording-process-host.js +1 -1
- package/dist/src/platform-runtime-unavailable.js +1 -1
- package/dist/src/platform-runtime.js +1 -1
- package/dist/src/platform-runtime2.js +1 -1
- package/dist/src/plugins.d.ts +9 -0
- package/dist/src/plugins.js +1 -0
- package/dist/src/plugins2.js +2 -0
- package/dist/src/png-crop-bytes.js +1 -1
- package/dist/src/png-size.js +1 -1
- package/dist/src/png-worker-client.js +1 -1
- package/dist/src/png.js +1 -1
- package/dist/src/prepare-kind.js +1 -1
- package/dist/src/process.js +1 -1
- package/dist/src/project-runtime.js +1 -1
- package/dist/src/provider-credential-fingerprint.js +1 -0
- package/dist/src/provider-device-admission.js +1 -1
- package/dist/src/provider-device-runtimes.js +1 -0
- package/dist/src/provider-limrun-credentials.js +1 -0
- package/dist/src/provider-profile-fields.js +1 -0
- package/dist/src/provider-session-ownership.js +2 -2
- package/dist/src/provider-webdriver.js +2 -1
- package/dist/src/providers.js +1 -1
- package/dist/src/proxy.js +1 -1
- package/dist/src/quality-warnings.js +1 -1
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-runtime.js +1 -1
- package/dist/src/record-trace.js +1 -1
- package/dist/src/recording.js +1 -1
- package/dist/src/rect-center.js +1 -1
- package/dist/src/register-builtins.js +1 -1
- package/dist/src/registry.js +52 -36
- package/dist/src/remote-config2.js +1 -1
- package/dist/src/replay-divergence.js +1 -1
- package/dist/src/replay.js +2 -2
- package/dist/src/reporting.js +5 -5
- package/dist/src/request-admission.js +1 -1
- package/dist/src/request-cancel.js +1 -1
- package/dist/src/request-cancellation.js +1 -1
- package/dist/src/request-runtime-binding.js +1 -1
- package/dist/src/result-serialization.js +1 -1
- package/dist/src/retry.js +1 -1
- package/dist/src/runner-artifact-manifest.js +1 -1
- package/dist/src/runner-cache-metadata.js +3 -3
- package/dist/src/runner-client.js +1 -1
- package/dist/src/runner-dev-tools-security.js +1 -1
- package/dist/src/runner-device-readiness.js +1 -1
- package/dist/src/runner-disposal.js +1 -1
- package/dist/src/runner-error-classification.js +2 -2
- package/dist/src/runner-exchange.js +1 -1
- package/dist/src/runner-operations-facade.js +1 -1
- package/dist/src/runner-session-types.js +1 -1
- package/dist/src/runner-start-budget.js +1 -1
- package/dist/src/runner.js +1 -1
- package/dist/src/runtime-admission.js +1 -1
- package/dist/src/runtime-transport-hints.js +1 -1
- package/dist/src/runtime.js +1 -1
- package/dist/src/runtime10.js +2 -2
- package/dist/src/runtime2.js +3 -3
- package/dist/src/runtime3.js +1 -1
- package/dist/src/runtime4.js +5 -5
- package/dist/src/runtime5.js +1 -1
- package/dist/src/runtime6.js +1 -1
- package/dist/src/runtime7.js +1 -1
- package/dist/src/runtime8.js +1 -1
- package/dist/src/runtime9.js +1 -1
- package/dist/src/screen-recording-admission-ledger.js +1 -1
- package/dist/src/screen-recording-completion.js +1 -1
- package/dist/src/screen-recording-options.js +1 -1
- package/dist/src/screen-recording-resource-recovery.js +1 -1
- package/dist/src/screen-recording-transport.js +1 -1
- package/dist/src/screenshot-density.js +1 -1
- package/dist/src/screenshot-image.js +1 -1
- package/dist/src/screenshot-result.js +1 -1
- package/dist/src/screenshot-runtime.js +1 -1
- package/dist/src/screenshot.js +1 -1
- package/dist/src/screenshot3.js +1 -1
- package/dist/src/script.js +1 -1
- package/dist/src/scroll-command.js +1 -1
- package/dist/src/scroll-gesture.js +1 -1
- package/dist/src/sdk-batch-runner.js +2 -2
- package/dist/src/sdk-batch.js +2 -2
- package/dist/src/sdk-contracts.d.ts +47 -2
- package/dist/src/sdk-contracts.js +2 -2
- package/dist/src/sdk-device.js +1 -1
- package/dist/src/sdk-io.js +1 -1
- package/dist/src/sdk-remote-config.js +1 -1
- package/dist/src/sdk-selectors.d.ts +37 -8
- package/dist/src/selection.js +0 -0
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.d.ts +2 -2
- package/dist/src/selectors.js +1 -1
- package/dist/src/server.js +1 -1
- package/dist/src/session-allocation.js +1 -1
- package/dist/src/session-artifact-paths.js +1 -1
- package/dist/src/session-repair-tombstone.js +1 -1
- package/dist/src/session-routing.js +1 -1
- package/dist/src/session-runtime-admission.js +1 -1
- package/dist/src/session-snapshot.js +1 -1
- package/dist/src/session-stream.js +27 -0
- package/dist/src/session.js +1 -1
- package/dist/src/session2.js +5 -5
- package/dist/src/settings.js +1 -1
- package/dist/src/settings2.js +1 -1
- package/dist/src/simctl-settings-facade.js +1 -0
- package/dist/src/simctl-settings.js +1 -0
- package/dist/src/simctl.js +1 -1
- package/dist/src/simulator-boot.js +1 -1
- package/dist/src/simulator.js +1 -1
- package/dist/src/snapshot-diagnostics.js +1 -1
- package/dist/src/snapshot-runtime-binding.js +1 -1
- package/dist/src/snapshot-runtime.js +1 -1
- package/dist/src/snapshot-state.js +1 -1
- package/dist/src/snapshot2.js +1 -1
- package/dist/src/snapshot3.js +1 -1
- package/dist/src/snapshot4.js +1 -1
- package/dist/src/src.js +1 -1
- package/dist/src/src7.js +1 -1
- package/dist/src/src8.js +2 -2
- package/dist/src/store.js +1 -0
- package/dist/src/string-enum.js +1 -1
- package/dist/src/surface-snapshot.js +1 -1
- package/dist/src/takeover.js +1 -1
- package/dist/src/target-evidence.js +1 -1
- package/dist/src/telemetry.js +1 -1
- package/dist/src/text-entry-focus.js +1 -1
- package/dist/src/tool-provider.js +3 -3
- package/dist/src/tool-provider2.js +1 -1
- package/dist/src/tool-provider3.js +1 -1
- package/dist/src/toolchain-probe.js +2 -2
- package/dist/src/touch-runtime.js +1 -1
- package/dist/src/touch-runtime2.js +1 -1
- package/dist/src/transport.js +2 -2
- package/dist/src/tv-remote.js +1 -1
- package/dist/src/type-text-runtime.js +1 -1
- package/dist/src/update-check.js +1 -1
- package/dist/src/vars.js +1 -1
- package/dist/src/verified-file.js +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/video.js +1 -1
- package/dist/src/viewport.js +1 -1
- package/dist/src/web.js +1 -1
- package/dist/src/web2.js +1 -1
- package/dist/src/webdriver-source.js +1 -1
- package/package.json +5 -1
- package/server.json +2 -2
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.19.apk.sha256 +0 -1
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.19.apk +0 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.19.apk.sha256 +0 -1
- package/dist/src/api.js +0 -3
- package/dist/src/daemon-process.js +0 -1
- package/dist/src/multitouch-support.js +0 -1
- package/dist/src/parameterized-recorded-fill.js +0 -1
- package/dist/src/runner-startup-transport.js +0 -1
- package/dist/src/runner-xctestrun.js +0 -1
- package/dist/src/session-store.js +0 -4
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
c9398faaf3bd6f4f7870e074d870d93c00892c849a8f523224a4b7705759edde agent-device-android-ime-helper-0.21.21.apk
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "android-ime-helper",
|
|
3
|
-
"version": "0.21.
|
|
4
|
-
"assetName": "agent-device-android-ime-helper-0.21.
|
|
5
|
-
"sha256": "
|
|
3
|
+
"version": "0.21.21",
|
|
4
|
+
"assetName": "agent-device-android-ime-helper-0.21.21.apk",
|
|
5
|
+
"sha256": "c9398faaf3bd6f4f7870e074d870d93c00892c849a8f523224a4b7705759edde",
|
|
6
6
|
"packageName": "com.callstack.agentdevice.imehelper",
|
|
7
|
-
"versionCode":
|
|
7
|
+
"versionCode": 21021,
|
|
8
8
|
"serviceComponent": "com.callstack.agentdevice.imehelper/.TestInputMethodService",
|
|
9
9
|
"broadcastProtocol": "android-ime-helper-v1"
|
|
10
10
|
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
afe24440c4392f50bd97c03024f256528699743d0a1bbad342ef163def2212b0 agent-device-android-snapshot-helper-0.21.21.apk
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "android-snapshot-helper",
|
|
3
|
-
"version": "0.21.
|
|
4
|
-
"releaseTag": "v0.21.
|
|
5
|
-
"assetName": "agent-device-android-snapshot-helper-0.21.
|
|
3
|
+
"version": "0.21.21",
|
|
4
|
+
"releaseTag": "v0.21.21",
|
|
5
|
+
"assetName": "agent-device-android-snapshot-helper-0.21.21.apk",
|
|
6
6
|
"apkUrl": null,
|
|
7
|
-
"sha256": "
|
|
8
|
-
"checksumName": "agent-device-android-snapshot-helper-0.21.
|
|
7
|
+
"sha256": "afe24440c4392f50bd97c03024f256528699743d0a1bbad342ef163def2212b0",
|
|
8
|
+
"checksumName": "agent-device-android-snapshot-helper-0.21.21.apk.sha256",
|
|
9
9
|
"packageName": "com.callstack.agentdevice.snapshothelper",
|
|
10
|
-
"versionCode":
|
|
10
|
+
"versionCode": 21021,
|
|
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
|
+
}
|