agent-device 0.10.0 → 0.10.2
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/README.md +4 -607
- package/dist/src/331.js +3 -3
- package/dist/src/425.js +1 -0
- package/dist/src/bin.js +28 -28
- package/dist/src/core/dispatch.d.ts +2 -0
- package/dist/src/core/session-surface.d.ts +3 -0
- package/dist/src/core/settings-contract.d.ts +2 -1
- package/dist/src/daemon/android-system-dialog.d.ts +11 -0
- package/dist/src/daemon/app-log-ios.d.ts +2 -1
- package/dist/src/daemon/app-log-process.d.ts +1 -1
- package/dist/src/daemon/app-log.d.ts +1 -1
- package/dist/src/daemon/context.d.ts +2 -0
- package/dist/src/daemon/handlers/interaction-common.d.ts +30 -1
- package/dist/src/daemon/handlers/interaction-read.d.ts +14 -0
- package/dist/src/daemon/handlers/interaction-touch.d.ts +45 -0
- package/dist/src/daemon/handlers/interaction.d.ts +2 -0
- package/dist/src/daemon/handlers/record-trace-android.d.ts +18 -0
- package/dist/src/daemon/handlers/record-trace-ios.d.ts +52 -0
- package/dist/src/daemon/handlers/record-trace-recording.d.ts +32 -0
- package/dist/src/daemon/handlers/record-trace.d.ts +2 -7
- package/dist/src/daemon/handlers/snapshot-capture.d.ts +11 -4
- package/dist/src/daemon/record-trace-errors.d.ts +6 -0
- package/dist/src/daemon/recording-gestures.d.ts +3 -0
- package/dist/src/daemon/recording-telemetry.d.ts +20 -0
- package/dist/src/daemon/recording-timing.d.ts +24 -0
- package/dist/src/daemon/request-router.d.ts +6 -0
- package/dist/src/daemon/script-utils.d.ts +1 -0
- package/dist/src/daemon/snapshot-processing.d.ts +1 -0
- package/dist/src/daemon/touch-reference-frame.d.ts +7 -0
- package/dist/src/daemon/types.d.ts +65 -11
- package/dist/src/daemon.js +62 -36
- package/dist/src/platforms/android/index.d.ts +1 -1
- package/dist/src/platforms/android/input-actions.d.ts +5 -0
- package/dist/src/platforms/android/settings.d.ts +1 -1
- package/dist/src/platforms/ios/apps.d.ts +1 -1
- package/dist/src/platforms/ios/macos-helper.d.ts +69 -0
- package/dist/src/platforms/ios/runner-client.d.ts +2 -2
- package/dist/src/platforms/ios/runner-session.d.ts +5 -0
- package/dist/src/platforms/ios/runner-xctestrun.d.ts +3 -1
- package/dist/src/recording/overlay.d.ts +10 -0
- package/dist/src/utils/command-schema.d.ts +2 -0
- package/dist/src/utils/interactors.d.ts +8 -8
- package/dist/src/utils/snapshot-lines.d.ts +5 -2
- package/dist/src/utils/snapshot.d.ts +8 -1
- package/dist/src/utils/text-surface.d.ts +19 -0
- package/dist/src/utils/video.d.ts +9 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +196 -51
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +133 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +1 -1
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +33 -1
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +4 -6
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +1 -0
- package/ios-runner/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +571 -0
- package/ios-runner/AgentDeviceRunner/RecordingScripts/recording-trim.swift +140 -0
- package/macos-helper/Package.swift +18 -0
- package/macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift +543 -0
- package/macos-helper/Sources/AgentDeviceMacOSHelper/main.swift +545 -0
- package/package.json +4 -1
- package/skills/agent-device/SKILL.md +25 -334
- package/skills/agent-device/references/bootstrap-install.md +167 -0
- package/skills/agent-device/references/coordinate-system.md +24 -4
- package/skills/agent-device/references/debugging.md +115 -0
- package/skills/agent-device/references/exploration.md +193 -0
- package/skills/agent-device/references/macos-desktop.md +55 -57
- package/skills/agent-device/references/remote-tenancy.md +56 -47
- package/skills/agent-device/references/verification.md +103 -0
- package/dist/src/274.js +0 -1
- package/dist/src/daemon/handlers/interaction-fill.d.ts +0 -3
- package/dist/src/daemon/handlers/interaction-press.d.ts +0 -3
- package/skills/agent-device/references/batching.md +0 -79
- package/skills/agent-device/references/logs-and-debug.md +0 -113
- package/skills/agent-device/references/perf-metrics.md +0 -53
- package/skills/agent-device/references/permissions.md +0 -70
- package/skills/agent-device/references/session-management.md +0 -101
- package/skills/agent-device/references/snapshot-refs.md +0 -102
- package/skills/agent-device/references/video-recording.md +0 -41
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "agent-device-macos-helper",
|
|
6
|
+
platforms: [.macOS(.v13)],
|
|
7
|
+
products: [
|
|
8
|
+
.executable(
|
|
9
|
+
name: "agent-device-macos-helper",
|
|
10
|
+
targets: ["AgentDeviceMacOSHelper"]
|
|
11
|
+
),
|
|
12
|
+
],
|
|
13
|
+
targets: [
|
|
14
|
+
.executableTarget(
|
|
15
|
+
name: "AgentDeviceMacOSHelper"
|
|
16
|
+
),
|
|
17
|
+
]
|
|
18
|
+
)
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import ApplicationServices
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
private enum SnapshotTraversalLimits {
|
|
6
|
+
static let maxDesktopApps = 24
|
|
7
|
+
static let maxNodes = 1500
|
|
8
|
+
static let maxDepth = 12
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
struct RectResponse: Encodable {
|
|
12
|
+
let x: Double
|
|
13
|
+
let y: Double
|
|
14
|
+
let width: Double
|
|
15
|
+
let height: Double
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
struct SnapshotNodeResponse: Encodable {
|
|
19
|
+
let index: Int
|
|
20
|
+
let type: String?
|
|
21
|
+
let role: String?
|
|
22
|
+
let subrole: String?
|
|
23
|
+
let label: String?
|
|
24
|
+
let value: String?
|
|
25
|
+
let identifier: String?
|
|
26
|
+
let rect: RectResponse?
|
|
27
|
+
let enabled: Bool?
|
|
28
|
+
let selected: Bool?
|
|
29
|
+
let hittable: Bool?
|
|
30
|
+
let depth: Int
|
|
31
|
+
let parentIndex: Int?
|
|
32
|
+
let pid: Int32?
|
|
33
|
+
let bundleId: String?
|
|
34
|
+
let appName: String?
|
|
35
|
+
let windowTitle: String?
|
|
36
|
+
let surface: String?
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
struct SnapshotResponse: Encodable {
|
|
40
|
+
let surface: String
|
|
41
|
+
let nodes: [SnapshotNodeResponse]
|
|
42
|
+
let truncated: Bool
|
|
43
|
+
let backend = "macos-helper"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private struct SnapshotBuildResult {
|
|
47
|
+
let nodes: [SnapshotNodeResponse]
|
|
48
|
+
let truncated: Bool
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private struct SnapshotContext {
|
|
52
|
+
let surface: String
|
|
53
|
+
let pid: Int32?
|
|
54
|
+
let bundleId: String?
|
|
55
|
+
let appName: String?
|
|
56
|
+
let windowTitle: String?
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private struct SnapshotTraversalState {
|
|
60
|
+
var nodes: [SnapshotNodeResponse] = []
|
|
61
|
+
var visited: [AXUIElement] = []
|
|
62
|
+
var truncated = false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func captureSnapshotResponse(surface: String) throws -> SnapshotResponse {
|
|
66
|
+
let result: SnapshotBuildResult
|
|
67
|
+
switch surface {
|
|
68
|
+
case "frontmost-app":
|
|
69
|
+
result = try snapshotFrontmostApp()
|
|
70
|
+
case "desktop":
|
|
71
|
+
result = snapshotDesktop()
|
|
72
|
+
case "menubar":
|
|
73
|
+
result = snapshotMenuBar()
|
|
74
|
+
default:
|
|
75
|
+
throw HelperError.invalidArgs("snapshot requires --surface <frontmost-app|desktop|menubar>")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return SnapshotResponse(surface: surface, nodes: result.nodes, truncated: result.truncated)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private func snapshotFrontmostApp() throws -> SnapshotBuildResult {
|
|
82
|
+
let app = try resolveTargetApplication(bundleId: nil, surface: "frontmost-app")
|
|
83
|
+
var state = SnapshotTraversalState()
|
|
84
|
+
_ = appendApplicationSnapshot(
|
|
85
|
+
app,
|
|
86
|
+
depth: 0,
|
|
87
|
+
parentIndex: nil,
|
|
88
|
+
surface: "frontmost-app",
|
|
89
|
+
state: &state
|
|
90
|
+
)
|
|
91
|
+
return SnapshotBuildResult(nodes: state.nodes, truncated: state.truncated)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private func snapshotDesktop() -> SnapshotBuildResult {
|
|
95
|
+
var state = SnapshotTraversalState()
|
|
96
|
+
guard
|
|
97
|
+
let rootIndex = appendSyntheticSnapshotNode(
|
|
98
|
+
into: &state,
|
|
99
|
+
type: "DesktopSurface",
|
|
100
|
+
label: "Desktop",
|
|
101
|
+
depth: 0,
|
|
102
|
+
parentIndex: nil,
|
|
103
|
+
surface: "desktop"
|
|
104
|
+
)
|
|
105
|
+
else {
|
|
106
|
+
return SnapshotBuildResult(nodes: state.nodes, truncated: true)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
var runningApps = NSWorkspace.shared.runningApplications.filter { app in
|
|
110
|
+
app.activationPolicy != .prohibited
|
|
111
|
+
&& !app.isTerminated
|
|
112
|
+
&& (app.bundleIdentifier?.isEmpty == false || app.localizedName?.isEmpty == false)
|
|
113
|
+
}
|
|
114
|
+
runningApps.sort { left, right in
|
|
115
|
+
if left.isActive != right.isActive {
|
|
116
|
+
return left.isActive && !right.isActive
|
|
117
|
+
}
|
|
118
|
+
return (left.localizedName ?? "") < (right.localizedName ?? "")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
var includedApps = 0
|
|
122
|
+
for app in runningApps {
|
|
123
|
+
if includedApps >= SnapshotTraversalLimits.maxDesktopApps {
|
|
124
|
+
state.truncated = true
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
if state.truncated {
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let included = appendApplicationSnapshot(
|
|
132
|
+
app,
|
|
133
|
+
depth: 1,
|
|
134
|
+
parentIndex: rootIndex,
|
|
135
|
+
surface: "desktop",
|
|
136
|
+
state: &state
|
|
137
|
+
)
|
|
138
|
+
if state.truncated {
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
if included {
|
|
142
|
+
includedApps += 1
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return SnapshotBuildResult(nodes: state.nodes, truncated: state.truncated)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@discardableResult
|
|
150
|
+
private func appendApplicationSnapshot(
|
|
151
|
+
_ app: NSRunningApplication,
|
|
152
|
+
depth: Int,
|
|
153
|
+
parentIndex: Int?,
|
|
154
|
+
surface: String,
|
|
155
|
+
state: inout SnapshotTraversalState
|
|
156
|
+
) -> Bool {
|
|
157
|
+
let appElement = AXUIElementCreateApplication(app.processIdentifier)
|
|
158
|
+
let visibleWindows = windows(of: appElement).filter(isVisibleSnapshotWindow)
|
|
159
|
+
if visibleWindows.isEmpty {
|
|
160
|
+
return false
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
guard
|
|
164
|
+
let appIndex = appendSyntheticSnapshotNode(
|
|
165
|
+
into: &state,
|
|
166
|
+
type: "Application",
|
|
167
|
+
label: app.localizedName ?? app.bundleIdentifier ?? "Application",
|
|
168
|
+
depth: depth,
|
|
169
|
+
parentIndex: parentIndex,
|
|
170
|
+
surface: surface,
|
|
171
|
+
identifier: app.bundleIdentifier,
|
|
172
|
+
pid: Int32(app.processIdentifier),
|
|
173
|
+
bundleId: app.bundleIdentifier,
|
|
174
|
+
appName: app.localizedName
|
|
175
|
+
)
|
|
176
|
+
else {
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for window in visibleWindows {
|
|
181
|
+
if state.truncated {
|
|
182
|
+
break
|
|
183
|
+
}
|
|
184
|
+
let windowTitle = stringAttribute(window, attribute: kAXTitleAttribute as String)
|
|
185
|
+
_ = appendElementSnapshot(
|
|
186
|
+
window,
|
|
187
|
+
depth: depth + 1,
|
|
188
|
+
parentIndex: appIndex,
|
|
189
|
+
context: SnapshotContext(
|
|
190
|
+
surface: surface,
|
|
191
|
+
pid: Int32(app.processIdentifier),
|
|
192
|
+
bundleId: app.bundleIdentifier,
|
|
193
|
+
appName: app.localizedName,
|
|
194
|
+
windowTitle: windowTitle
|
|
195
|
+
),
|
|
196
|
+
state: &state
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private func snapshotMenuBar() -> SnapshotBuildResult {
|
|
204
|
+
var state = SnapshotTraversalState()
|
|
205
|
+
guard
|
|
206
|
+
let rootIndex = appendSyntheticSnapshotNode(
|
|
207
|
+
into: &state,
|
|
208
|
+
type: "MenuBarSurface",
|
|
209
|
+
label: "Menu Bar",
|
|
210
|
+
depth: 0,
|
|
211
|
+
parentIndex: nil,
|
|
212
|
+
surface: "menubar"
|
|
213
|
+
)
|
|
214
|
+
else {
|
|
215
|
+
return SnapshotBuildResult(nodes: state.nodes, truncated: true)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if let frontmost = NSWorkspace.shared.frontmostApplication {
|
|
219
|
+
let frontmostElement = AXUIElementCreateApplication(frontmost.processIdentifier)
|
|
220
|
+
if let menuBar = elementAttribute(frontmostElement, attribute: kAXMenuBarAttribute as String) {
|
|
221
|
+
_ = appendElementSnapshot(
|
|
222
|
+
menuBar,
|
|
223
|
+
depth: 1,
|
|
224
|
+
parentIndex: rootIndex,
|
|
225
|
+
context: SnapshotContext(
|
|
226
|
+
surface: "menubar",
|
|
227
|
+
pid: Int32(frontmost.processIdentifier),
|
|
228
|
+
bundleId: frontmost.bundleIdentifier,
|
|
229
|
+
appName: frontmost.localizedName,
|
|
230
|
+
windowTitle: frontmost.localizedName
|
|
231
|
+
),
|
|
232
|
+
state: &state
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if !state.truncated,
|
|
238
|
+
let systemUiServer = NSRunningApplication.runningApplications(
|
|
239
|
+
withBundleIdentifier: "com.apple.systemuiserver"
|
|
240
|
+
).first
|
|
241
|
+
{
|
|
242
|
+
let systemUiElement = AXUIElementCreateApplication(systemUiServer.processIdentifier)
|
|
243
|
+
if let menuExtras = elementAttribute(systemUiElement, attribute: kAXMenuBarAttribute as String) {
|
|
244
|
+
_ = appendElementSnapshot(
|
|
245
|
+
menuExtras,
|
|
246
|
+
depth: 1,
|
|
247
|
+
parentIndex: rootIndex,
|
|
248
|
+
context: SnapshotContext(
|
|
249
|
+
surface: "menubar",
|
|
250
|
+
pid: Int32(systemUiServer.processIdentifier),
|
|
251
|
+
bundleId: systemUiServer.bundleIdentifier,
|
|
252
|
+
appName: systemUiServer.localizedName,
|
|
253
|
+
windowTitle: "System Menu Extras"
|
|
254
|
+
),
|
|
255
|
+
state: &state
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return SnapshotBuildResult(nodes: state.nodes, truncated: state.truncated)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@discardableResult
|
|
264
|
+
private func appendSyntheticSnapshotNode(
|
|
265
|
+
into state: inout SnapshotTraversalState,
|
|
266
|
+
type: String,
|
|
267
|
+
label: String,
|
|
268
|
+
depth: Int,
|
|
269
|
+
parentIndex: Int?,
|
|
270
|
+
surface: String,
|
|
271
|
+
identifier: String? = nil,
|
|
272
|
+
pid: Int32? = nil,
|
|
273
|
+
bundleId: String? = nil,
|
|
274
|
+
appName: String? = nil,
|
|
275
|
+
windowTitle: String? = nil
|
|
276
|
+
) -> Int? {
|
|
277
|
+
guard reserveSnapshotNodeCapacity(&state) else {
|
|
278
|
+
return nil
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let index = state.nodes.count
|
|
282
|
+
state.nodes.append(
|
|
283
|
+
SnapshotNodeResponse(
|
|
284
|
+
index: index,
|
|
285
|
+
type: type,
|
|
286
|
+
role: type,
|
|
287
|
+
subrole: nil,
|
|
288
|
+
label: label,
|
|
289
|
+
value: nil,
|
|
290
|
+
identifier: identifier ?? "surface:\(surface):\(type.lowercased())",
|
|
291
|
+
rect: nil,
|
|
292
|
+
enabled: true,
|
|
293
|
+
selected: nil,
|
|
294
|
+
hittable: false,
|
|
295
|
+
depth: depth,
|
|
296
|
+
parentIndex: parentIndex,
|
|
297
|
+
pid: pid,
|
|
298
|
+
bundleId: bundleId,
|
|
299
|
+
appName: appName,
|
|
300
|
+
windowTitle: windowTitle,
|
|
301
|
+
surface: surface
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
return index
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
@discardableResult
|
|
308
|
+
private func appendElementSnapshot(
|
|
309
|
+
_ element: AXUIElement,
|
|
310
|
+
depth: Int,
|
|
311
|
+
parentIndex: Int?,
|
|
312
|
+
context: SnapshotContext,
|
|
313
|
+
state: inout SnapshotTraversalState,
|
|
314
|
+
maxDepth: Int = SnapshotTraversalLimits.maxDepth
|
|
315
|
+
) -> Int? {
|
|
316
|
+
if state.visited.contains(where: { CFEqual($0, element) }) {
|
|
317
|
+
return parentIndex
|
|
318
|
+
}
|
|
319
|
+
guard reserveSnapshotNodeCapacity(&state) else {
|
|
320
|
+
return parentIndex
|
|
321
|
+
}
|
|
322
|
+
state.visited.append(element)
|
|
323
|
+
|
|
324
|
+
let role = stringAttribute(element, attribute: kAXRoleAttribute as String)
|
|
325
|
+
let subrole = stringAttribute(element, attribute: kAXSubroleAttribute as String)
|
|
326
|
+
let title = stringAttribute(element, attribute: kAXTitleAttribute as String)
|
|
327
|
+
let description = stringAttribute(element, attribute: kAXDescriptionAttribute as String)
|
|
328
|
+
let value = stringAttribute(element, attribute: kAXValueAttribute as String)
|
|
329
|
+
let identifier = stringAttribute(element, attribute: "AXIdentifier")
|
|
330
|
+
let rect = rectAttribute(element)
|
|
331
|
+
let enabled = boolAttribute(element, attribute: kAXEnabledAttribute as String)
|
|
332
|
+
let selected = boolAttribute(element, attribute: kAXSelectedAttribute as String)
|
|
333
|
+
let type = normalizedSnapshotType(role: role, subrole: subrole)
|
|
334
|
+
let windowTitle = context.windowTitle ?? inferWindowTitle(for: element)
|
|
335
|
+
|
|
336
|
+
let index = state.nodes.count
|
|
337
|
+
state.nodes.append(
|
|
338
|
+
SnapshotNodeResponse(
|
|
339
|
+
index: index,
|
|
340
|
+
type: type,
|
|
341
|
+
role: role,
|
|
342
|
+
subrole: subrole,
|
|
343
|
+
label: title ?? description ?? value,
|
|
344
|
+
value: value,
|
|
345
|
+
identifier: identifier,
|
|
346
|
+
rect: rect,
|
|
347
|
+
enabled: enabled,
|
|
348
|
+
selected: selected,
|
|
349
|
+
hittable: (enabled ?? true) && rect != nil,
|
|
350
|
+
depth: depth,
|
|
351
|
+
parentIndex: parentIndex,
|
|
352
|
+
pid: context.pid,
|
|
353
|
+
bundleId: context.bundleId,
|
|
354
|
+
appName: context.appName,
|
|
355
|
+
windowTitle: windowTitle,
|
|
356
|
+
surface: context.surface
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
guard depth < maxDepth, !state.truncated else {
|
|
361
|
+
return index
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
for child in children(of: element) {
|
|
365
|
+
if state.truncated {
|
|
366
|
+
break
|
|
367
|
+
}
|
|
368
|
+
_ = appendElementSnapshot(
|
|
369
|
+
child,
|
|
370
|
+
depth: depth + 1,
|
|
371
|
+
parentIndex: index,
|
|
372
|
+
context: SnapshotContext(
|
|
373
|
+
surface: context.surface,
|
|
374
|
+
pid: context.pid,
|
|
375
|
+
bundleId: context.bundleId,
|
|
376
|
+
appName: context.appName,
|
|
377
|
+
windowTitle: windowTitle
|
|
378
|
+
),
|
|
379
|
+
state: &state,
|
|
380
|
+
maxDepth: maxDepth
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return index
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private func reserveSnapshotNodeCapacity(_ state: inout SnapshotTraversalState) -> Bool {
|
|
388
|
+
if state.nodes.count >= SnapshotTraversalLimits.maxNodes {
|
|
389
|
+
state.truncated = true
|
|
390
|
+
return false
|
|
391
|
+
}
|
|
392
|
+
return true
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private func normalizedSnapshotType(role: String?, subrole: String?) -> String? {
|
|
396
|
+
switch role {
|
|
397
|
+
case "AXApplication":
|
|
398
|
+
return "Application"
|
|
399
|
+
case "AXWindow":
|
|
400
|
+
return subrole == "AXStandardWindow" ? "Window" : (subrole ?? "Window")
|
|
401
|
+
case "AXSheet":
|
|
402
|
+
return "Sheet"
|
|
403
|
+
case "AXDialog":
|
|
404
|
+
return "Dialog"
|
|
405
|
+
case "AXButton":
|
|
406
|
+
return "Button"
|
|
407
|
+
case "AXStaticText":
|
|
408
|
+
return "StaticText"
|
|
409
|
+
case "AXTextField":
|
|
410
|
+
return "TextField"
|
|
411
|
+
case "AXTextArea":
|
|
412
|
+
return "TextArea"
|
|
413
|
+
case "AXScrollArea":
|
|
414
|
+
return "ScrollArea"
|
|
415
|
+
case "AXGroup":
|
|
416
|
+
return "Group"
|
|
417
|
+
case "AXMenuBar":
|
|
418
|
+
return "MenuBar"
|
|
419
|
+
case "AXMenuBarItem":
|
|
420
|
+
return "MenuBarItem"
|
|
421
|
+
case "AXMenu":
|
|
422
|
+
return "Menu"
|
|
423
|
+
case "AXMenuItem":
|
|
424
|
+
return "MenuItem"
|
|
425
|
+
default:
|
|
426
|
+
if let subrole, !subrole.isEmpty {
|
|
427
|
+
return subrole
|
|
428
|
+
}
|
|
429
|
+
return role
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private func isVisibleSnapshotWindow(_ window: AXUIElement) -> Bool {
|
|
434
|
+
guard let rect = rectAttribute(window) else {
|
|
435
|
+
return false
|
|
436
|
+
}
|
|
437
|
+
if rect.width <= 0 || rect.height <= 0 {
|
|
438
|
+
return false
|
|
439
|
+
}
|
|
440
|
+
if boolAttribute(window, attribute: kAXMinimizedAttribute as String) == true {
|
|
441
|
+
return false
|
|
442
|
+
}
|
|
443
|
+
return true
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private func inferWindowTitle(for element: AXUIElement) -> String? {
|
|
447
|
+
if let title = stringAttribute(element, attribute: kAXTitleAttribute as String) {
|
|
448
|
+
return title
|
|
449
|
+
}
|
|
450
|
+
if let window = elementAttribute(element, attribute: kAXWindowAttribute as String) {
|
|
451
|
+
return stringAttribute(window, attribute: kAXTitleAttribute as String)
|
|
452
|
+
}
|
|
453
|
+
return nil
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
func stringAttribute(_ element: AXUIElement, attribute: String) -> String? {
|
|
457
|
+
var value: CFTypeRef?
|
|
458
|
+
guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success else {
|
|
459
|
+
return nil
|
|
460
|
+
}
|
|
461
|
+
if let text = value as? String {
|
|
462
|
+
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
463
|
+
return trimmed.isEmpty ? nil : trimmed
|
|
464
|
+
}
|
|
465
|
+
return nil
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
func boolAttribute(_ element: AXUIElement, attribute: String) -> Bool? {
|
|
469
|
+
var value: CFTypeRef?
|
|
470
|
+
guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
|
|
471
|
+
let number = value as? NSNumber
|
|
472
|
+
else {
|
|
473
|
+
return nil
|
|
474
|
+
}
|
|
475
|
+
return number.boolValue
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
func elementAttribute(_ element: AXUIElement, attribute: String) -> AXUIElement? {
|
|
479
|
+
var value: CFTypeRef?
|
|
480
|
+
guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
|
|
481
|
+
let value,
|
|
482
|
+
CFGetTypeID(value) == AXUIElementGetTypeID()
|
|
483
|
+
else {
|
|
484
|
+
return nil
|
|
485
|
+
}
|
|
486
|
+
return (value as! AXUIElement)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
func rectAttribute(_ element: AXUIElement) -> RectResponse? {
|
|
490
|
+
var positionValue: CFTypeRef?
|
|
491
|
+
var sizeValue: CFTypeRef?
|
|
492
|
+
guard AXUIElementCopyAttributeValue(element, kAXPositionAttribute as CFString, &positionValue) == .success,
|
|
493
|
+
AXUIElementCopyAttributeValue(element, kAXSizeAttribute as CFString, &sizeValue) == .success,
|
|
494
|
+
let positionAxValue = accessibilityAxValue(positionValue),
|
|
495
|
+
let sizeAxValue = accessibilityAxValue(sizeValue)
|
|
496
|
+
else {
|
|
497
|
+
return nil
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
var position = CGPoint.zero
|
|
501
|
+
var size = CGSize.zero
|
|
502
|
+
guard AXValueGetType(positionAxValue) == .cgPoint,
|
|
503
|
+
AXValueGetValue(positionAxValue, .cgPoint, &position),
|
|
504
|
+
AXValueGetType(sizeAxValue) == .cgSize,
|
|
505
|
+
AXValueGetValue(sizeAxValue, .cgSize, &size)
|
|
506
|
+
else {
|
|
507
|
+
return nil
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return RectResponse(
|
|
511
|
+
x: Double(position.x),
|
|
512
|
+
y: Double(position.y),
|
|
513
|
+
width: Double(size.width),
|
|
514
|
+
height: Double(size.height)
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private func accessibilityAxValue(_ value: CFTypeRef?) -> AXValue? {
|
|
519
|
+
guard let value, CFGetTypeID(value) == AXValueGetTypeID() else {
|
|
520
|
+
return nil
|
|
521
|
+
}
|
|
522
|
+
return (value as! AXValue)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
func children(of element: AXUIElement) -> [AXUIElement] {
|
|
526
|
+
var value: CFTypeRef?
|
|
527
|
+
guard AXUIElementCopyAttributeValue(element, kAXChildrenAttribute as CFString, &value) == .success,
|
|
528
|
+
let children = value as? [AXUIElement]
|
|
529
|
+
else {
|
|
530
|
+
return []
|
|
531
|
+
}
|
|
532
|
+
return children
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
func windows(of appElement: AXUIElement) -> [AXUIElement] {
|
|
536
|
+
var value: CFTypeRef?
|
|
537
|
+
guard AXUIElementCopyAttributeValue(appElement, "AXWindows" as CFString, &value) == .success,
|
|
538
|
+
let windows = value as? [AXUIElement]
|
|
539
|
+
else {
|
|
540
|
+
return []
|
|
541
|
+
}
|
|
542
|
+
return windows
|
|
543
|
+
}
|