@anionex/dsh-computer-use 0.1.0
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +307 -0
- package/README.zh.md +307 -0
- package/assets/computer-use-fixture.png +0 -0
- package/assets/cursor.png +0 -0
- package/cordis.patch.yml +13 -0
- package/docs/interaction-policy.i18n.yaml +4 -0
- package/docs/interaction-policy.md +140 -0
- package/docs/interaction-policy.zh.md +140 -0
- package/lib/approval-policy.js +12 -0
- package/lib/approval-policy.js.map +1 -0
- package/lib/artifacts.js +79 -0
- package/lib/artifacts.js.map +1 -0
- package/lib/backend.js +3 -0
- package/lib/backend.js.map +1 -0
- package/lib/client.js +349 -0
- package/lib/client.js.map +1 -0
- package/lib/config.js +120 -0
- package/lib/config.js.map +1 -0
- package/lib/confirmations.js +99 -0
- package/lib/confirmations.js.map +1 -0
- package/lib/diff.js +58 -0
- package/lib/diff.js.map +1 -0
- package/lib/errors.js +22 -0
- package/lib/errors.js.map +1 -0
- package/lib/exposure.js +231 -0
- package/lib/exposure.js.map +1 -0
- package/lib/index.js +56 -0
- package/lib/index.js.map +1 -0
- package/lib/leases.js +259 -0
- package/lib/leases.js.map +1 -0
- package/lib/providers/macos.js +148 -0
- package/lib/providers/macos.js.map +1 -0
- package/lib/providers/native-helper.js +356 -0
- package/lib/providers/native-helper.js.map +1 -0
- package/lib/service.js +480 -0
- package/lib/service.js.map +1 -0
- package/lib/skill.js +119 -0
- package/lib/skill.js.map +1 -0
- package/lib/target-resolver.js +180 -0
- package/lib/target-resolver.js.map +1 -0
- package/lib/tools.js +493 -0
- package/lib/tools.js.map +1 -0
- package/lib/types/approval-policy.d.ts +13 -0
- package/lib/types/approval-policy.d.ts.map +1 -0
- package/lib/types/artifacts.d.ts +10 -0
- package/lib/types/artifacts.d.ts.map +1 -0
- package/lib/types/backend.d.ts +94 -0
- package/lib/types/backend.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +78 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/config.d.ts +75 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/confirmations.d.ts +23 -0
- package/lib/types/confirmations.d.ts.map +1 -0
- package/lib/types/diff.d.ts +5 -0
- package/lib/types/diff.d.ts.map +1 -0
- package/lib/types/errors.d.ts +15 -0
- package/lib/types/errors.d.ts.map +1 -0
- package/lib/types/exposure.d.ts +30 -0
- package/lib/types/exposure.d.ts.map +1 -0
- package/lib/types/index.d.ts +21 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/leases.d.ts +80 -0
- package/lib/types/leases.d.ts.map +1 -0
- package/lib/types/providers/macos.d.ts +15 -0
- package/lib/types/providers/macos.d.ts.map +1 -0
- package/lib/types/providers/native-helper.d.ts +36 -0
- package/lib/types/providers/native-helper.d.ts.map +1 -0
- package/lib/types/service.d.ts +52 -0
- package/lib/types/service.d.ts.map +1 -0
- package/lib/types/skill.d.ts +9 -0
- package/lib/types/skill.d.ts.map +1 -0
- package/lib/types/target-resolver.d.ts +38 -0
- package/lib/types/target-resolver.d.ts.map +1 -0
- package/lib/types/tools.d.ts +6 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/types.d.ts +246 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/web.d.ts +32 -0
- package/lib/types/web.d.ts.map +1 -0
- package/lib/types.js +9 -0
- package/lib/types.js.map +1 -0
- package/lib/web.js +163 -0
- package/lib/web.js.map +1 -0
- package/native/macos/Sources/Fixture/main.swift +366 -0
- package/native/macos/Sources/Helper/CursorImage.swift +11 -0
- package/native/macos/Sources/Helper/CursorOverlay.swift +488 -0
- package/native/macos/Sources/Helper/TargetedPointer.swift +314 -0
- package/native/macos/Sources/Helper/main.swift +1140 -0
- package/native/macos/Sources/Monitor/main.swift +139 -0
- package/native/macos/bin/dsh-computer-use-helper +0 -0
- package/native/macos/manifest.json +14 -0
- package/package.json +128 -0
- package/scripts/build-client.mjs +24 -0
- package/scripts/build-native.mjs +158 -0
- package/scripts/check-native.mjs +92 -0
- package/scripts/model-e2e.mjs +291 -0
- package/scripts/session-transcript.mjs +117 -0
- package/scripts/validate.mjs +256 -0
- package/src/approval-policy.ts +16 -0
- package/src/artifacts.ts +93 -0
- package/src/backend.ts +103 -0
- package/src/client/index.tsx +539 -0
- package/src/config.ts +196 -0
- package/src/confirmations.ts +128 -0
- package/src/diff.ts +52 -0
- package/src/errors.ts +39 -0
- package/src/exposure.ts +236 -0
- package/src/index.ts +66 -0
- package/src/leases.ts +337 -0
- package/src/providers/macos.ts +185 -0
- package/src/providers/native-helper.ts +393 -0
- package/src/service.ts +583 -0
- package/src/skill.ts +123 -0
- package/src/target-resolver.ts +237 -0
- package/src/tools.ts +528 -0
- package/src/types.ts +299 -0
- package/src/web.ts +180 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import CoreGraphics
|
|
3
|
+
import Darwin
|
|
4
|
+
import Foundation
|
|
5
|
+
import QuartzCore
|
|
6
|
+
|
|
7
|
+
private struct CursorOverlayCommand {
|
|
8
|
+
let operation: String
|
|
9
|
+
let point: CGPoint?
|
|
10
|
+
let durationMs: Int
|
|
11
|
+
let autoHideMs: Int
|
|
12
|
+
let targetPid: pid_t?
|
|
13
|
+
let targetWindowNumber: Int64?
|
|
14
|
+
let targetWindowFrame: CGRect?
|
|
15
|
+
let sustainedPress: Bool
|
|
16
|
+
|
|
17
|
+
private static let maximumCoordinateMagnitude = 100_000.0
|
|
18
|
+
private static let maximumDurationMs = 2_000
|
|
19
|
+
private static let maximumAutoHideMs = 30_000
|
|
20
|
+
|
|
21
|
+
private static func number(_ object: Any?, field: String) throws -> NSNumber? {
|
|
22
|
+
guard let object else { return nil }
|
|
23
|
+
guard let value = object as? NSNumber,
|
|
24
|
+
CFGetTypeID(value) != CFBooleanGetTypeID() else {
|
|
25
|
+
throw CursorOverlayError(message: "cursor overlay \(field) must be numeric")
|
|
26
|
+
}
|
|
27
|
+
return value
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private static func integer(_ object: Any?, field: String, fallback: Int) throws -> Int {
|
|
31
|
+
guard let value = try number(object, field: field) else { return fallback }
|
|
32
|
+
let doubleValue = value.doubleValue
|
|
33
|
+
guard doubleValue.isFinite, doubleValue.rounded(.towardZero) == doubleValue,
|
|
34
|
+
doubleValue >= Double(Int.min), doubleValue <= Double(Int.max) else {
|
|
35
|
+
throw CursorOverlayError(message: "cursor overlay \(field) must be an integer")
|
|
36
|
+
}
|
|
37
|
+
return Int(doubleValue)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
init(_ object: [String: Any]) throws {
|
|
41
|
+
guard let operation = object["op"] as? String else {
|
|
42
|
+
throw CursorOverlayError(message: "cursor overlay command is missing op")
|
|
43
|
+
}
|
|
44
|
+
self.operation = operation
|
|
45
|
+
let x = try Self.number(object["x"], field: "x")
|
|
46
|
+
let y = try Self.number(object["y"], field: "y")
|
|
47
|
+
if let x, let y {
|
|
48
|
+
let xValue = x.doubleValue
|
|
49
|
+
let yValue = y.doubleValue
|
|
50
|
+
guard xValue.isFinite, yValue.isFinite,
|
|
51
|
+
abs(xValue) <= Self.maximumCoordinateMagnitude,
|
|
52
|
+
abs(yValue) <= Self.maximumCoordinateMagnitude else {
|
|
53
|
+
throw CursorOverlayError(message: "cursor overlay coordinates are outside the supported range")
|
|
54
|
+
}
|
|
55
|
+
self.point = CGPoint(x: xValue, y: yValue)
|
|
56
|
+
} else {
|
|
57
|
+
self.point = nil
|
|
58
|
+
}
|
|
59
|
+
let durationMs = try Self.integer(object["durationMs"], field: "durationMs", fallback: 180)
|
|
60
|
+
let autoHideMs = try Self.integer(object["autoHideMs"], field: "autoHideMs", fallback: 0)
|
|
61
|
+
guard durationMs >= 0, durationMs <= Self.maximumDurationMs,
|
|
62
|
+
autoHideMs >= 0, autoHideMs <= Self.maximumAutoHideMs else {
|
|
63
|
+
throw CursorOverlayError(message: "cursor overlay timing is outside the supported range")
|
|
64
|
+
}
|
|
65
|
+
self.durationMs = durationMs
|
|
66
|
+
self.autoHideMs = autoHideMs
|
|
67
|
+
|
|
68
|
+
if let rawPid = try Self.number(object["targetPid"], field: "targetPid") {
|
|
69
|
+
let pidValue = rawPid.doubleValue
|
|
70
|
+
guard pidValue.isFinite, pidValue.rounded(.towardZero) == pidValue,
|
|
71
|
+
pidValue > 0, pidValue <= Double(Int32.max) else {
|
|
72
|
+
throw CursorOverlayError(message: "cursor overlay targetPid is invalid")
|
|
73
|
+
}
|
|
74
|
+
self.targetPid = pid_t(pidValue)
|
|
75
|
+
} else {
|
|
76
|
+
self.targetPid = nil
|
|
77
|
+
}
|
|
78
|
+
if let rawWindowNumber = try Self.number(object["targetWindowNumber"], field: "targetWindowNumber") {
|
|
79
|
+
let windowValue = rawWindowNumber.doubleValue
|
|
80
|
+
guard windowValue.isFinite, windowValue.rounded(.towardZero) == windowValue,
|
|
81
|
+
windowValue > 0, windowValue <= Double(Int64.max) else {
|
|
82
|
+
throw CursorOverlayError(message: "cursor overlay targetWindowNumber is invalid")
|
|
83
|
+
}
|
|
84
|
+
self.targetWindowNumber = Int64(windowValue)
|
|
85
|
+
} else {
|
|
86
|
+
self.targetWindowNumber = nil
|
|
87
|
+
}
|
|
88
|
+
if let frame = object["targetWindowFrame"] as? [String: Any] {
|
|
89
|
+
guard let x = try Self.number(frame["x"], field: "targetWindowFrame.x"),
|
|
90
|
+
let y = try Self.number(frame["y"], field: "targetWindowFrame.y"),
|
|
91
|
+
let width = try Self.number(frame["width"], field: "targetWindowFrame.width"),
|
|
92
|
+
let height = try Self.number(frame["height"], field: "targetWindowFrame.height") else {
|
|
93
|
+
throw CursorOverlayError(message: "cursor overlay targetWindowFrame is incomplete")
|
|
94
|
+
}
|
|
95
|
+
let values = [x.doubleValue, y.doubleValue, width.doubleValue, height.doubleValue]
|
|
96
|
+
guard values.allSatisfy(\.isFinite), width.doubleValue > 0, height.doubleValue > 0,
|
|
97
|
+
values.allSatisfy({ abs($0) <= Self.maximumCoordinateMagnitude }) else {
|
|
98
|
+
throw CursorOverlayError(message: "cursor overlay targetWindowFrame is invalid")
|
|
99
|
+
}
|
|
100
|
+
self.targetWindowFrame = CGRect(
|
|
101
|
+
x: x.doubleValue,
|
|
102
|
+
y: y.doubleValue,
|
|
103
|
+
width: width.doubleValue,
|
|
104
|
+
height: height.doubleValue
|
|
105
|
+
)
|
|
106
|
+
} else {
|
|
107
|
+
self.targetWindowFrame = nil
|
|
108
|
+
}
|
|
109
|
+
if let sustainedPress = object["sustainedPress"] {
|
|
110
|
+
guard let value = sustainedPress as? Bool else {
|
|
111
|
+
throw CursorOverlayError(message: "cursor overlay sustainedPress must be boolean")
|
|
112
|
+
}
|
|
113
|
+
self.sustainedPress = value
|
|
114
|
+
} else {
|
|
115
|
+
self.sustainedPress = false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private struct CursorOverlayError: Error {
|
|
121
|
+
let message: String
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private final class CursorPanel: NSPanel {
|
|
125
|
+
override var canBecomeKey: Bool { false }
|
|
126
|
+
override var canBecomeMain: Bool { false }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private final class CursorView: NSView {
|
|
130
|
+
override var isFlipped: Bool { true }
|
|
131
|
+
|
|
132
|
+
private let image: NSImage? = EmbeddedCursorImage.image
|
|
133
|
+
|
|
134
|
+
var pressed = false {
|
|
135
|
+
didSet { needsDisplay = true }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
override var isOpaque: Bool { false }
|
|
139
|
+
|
|
140
|
+
override func draw(_ dirtyRect: NSRect) {
|
|
141
|
+
super.draw(dirtyRect)
|
|
142
|
+
guard let image else { return }
|
|
143
|
+
let imageRect = pressed ? bounds.insetBy(dx: 2, dy: 2) : bounds
|
|
144
|
+
image.draw(
|
|
145
|
+
in: imageRect,
|
|
146
|
+
from: .zero,
|
|
147
|
+
operation: .sourceOver,
|
|
148
|
+
fraction: pressed ? 0.85 : 1,
|
|
149
|
+
respectFlipped: true,
|
|
150
|
+
hints: nil
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@MainActor
|
|
156
|
+
private final class CursorOverlayController: NSObject {
|
|
157
|
+
private static let size = NSSize(width: 28, height: 28)
|
|
158
|
+
|
|
159
|
+
private let window: CursorPanel
|
|
160
|
+
private let cursorView: CursorView
|
|
161
|
+
private var hideWork: DispatchWorkItem?
|
|
162
|
+
private var releaseWork: DispatchWorkItem?
|
|
163
|
+
private var targetCheckTimer: Timer?
|
|
164
|
+
private var targetPid: pid_t?
|
|
165
|
+
private var targetWindowNumber: Int64?
|
|
166
|
+
private var targetWindowFrame: CGRect?
|
|
167
|
+
|
|
168
|
+
override init() {
|
|
169
|
+
cursorView = CursorView(frame: NSRect(origin: .zero, size: Self.size))
|
|
170
|
+
window = CursorPanel(
|
|
171
|
+
contentRect: NSRect(origin: .zero, size: Self.size),
|
|
172
|
+
styleMask: [.borderless, .nonactivatingPanel],
|
|
173
|
+
backing: .buffered,
|
|
174
|
+
defer: false
|
|
175
|
+
)
|
|
176
|
+
super.init()
|
|
177
|
+
window.title = "DSH Computer Use Cursor"
|
|
178
|
+
window.contentView = cursorView
|
|
179
|
+
window.isOpaque = false
|
|
180
|
+
window.backgroundColor = .clear
|
|
181
|
+
window.hasShadow = false
|
|
182
|
+
window.ignoresMouseEvents = true
|
|
183
|
+
window.hidesOnDeactivate = false
|
|
184
|
+
window.becomesKeyOnlyIfNeeded = true
|
|
185
|
+
window.level = .floating
|
|
186
|
+
window.collectionBehavior = [.fullScreenAuxiliary, .stationary, .ignoresCycle]
|
|
187
|
+
window.isReleasedWhenClosed = false
|
|
188
|
+
window.sharingType = .readOnly
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
func show(
|
|
192
|
+
at quartzPoint: CGPoint,
|
|
193
|
+
durationMs: Int,
|
|
194
|
+
autoHideMs: Int,
|
|
195
|
+
targetPid: pid_t?,
|
|
196
|
+
targetWindowNumber: Int64?,
|
|
197
|
+
targetWindowFrame: CGRect?
|
|
198
|
+
) {
|
|
199
|
+
hideWork?.cancel()
|
|
200
|
+
guard targetWindowIsCurrent(pid: targetPid, windowNumber: targetWindowNumber, expectedFrame: targetWindowFrame) else {
|
|
201
|
+
hide()
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
self.targetPid = targetPid
|
|
205
|
+
self.targetWindowNumber = targetWindowNumber
|
|
206
|
+
self.targetWindowFrame = targetWindowFrame
|
|
207
|
+
scheduleTargetChecks()
|
|
208
|
+
let point = appKitPoint(fromQuartz: quartzPoint)
|
|
209
|
+
let targetOrigin = NSPoint(
|
|
210
|
+
x: point.x,
|
|
211
|
+
y: point.y - Self.size.height
|
|
212
|
+
)
|
|
213
|
+
if window.isVisible && durationMs > 0 {
|
|
214
|
+
NSAnimationContext.runAnimationGroup { context in
|
|
215
|
+
context.duration = Double(durationMs) / 1000
|
|
216
|
+
context.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
|
217
|
+
window.animator().setFrameOrigin(targetOrigin)
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
window.setFrameOrigin(targetOrigin)
|
|
221
|
+
window.orderFrontRegardless()
|
|
222
|
+
}
|
|
223
|
+
scheduleHide(after: autoHideMs)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
func press(autoHideMs: Int, sustained: Bool) {
|
|
227
|
+
guard window.isVisible else { return }
|
|
228
|
+
releaseWork?.cancel()
|
|
229
|
+
releaseWork = nil
|
|
230
|
+
cursorView.pressed = true
|
|
231
|
+
if !sustained {
|
|
232
|
+
let work = DispatchWorkItem { [weak self] in
|
|
233
|
+
self?.cursorView.pressed = false
|
|
234
|
+
}
|
|
235
|
+
releaseWork = work
|
|
236
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(150), execute: work)
|
|
237
|
+
}
|
|
238
|
+
scheduleHide(after: autoHideMs)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
func release(autoHideMs: Int) {
|
|
242
|
+
releaseWork?.cancel()
|
|
243
|
+
releaseWork = nil
|
|
244
|
+
cursorView.pressed = false
|
|
245
|
+
scheduleHide(after: autoHideMs)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
func validateTarget(pid: pid_t?, windowNumber: Int64?, expectedFrame: CGRect?) {
|
|
249
|
+
guard window.isVisible else { return }
|
|
250
|
+
if !targetWindowIsCurrent(pid: pid, windowNumber: windowNumber, expectedFrame: expectedFrame) { hide() }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
func hide() {
|
|
254
|
+
hideWork?.cancel()
|
|
255
|
+
hideWork = nil
|
|
256
|
+
releaseWork?.cancel()
|
|
257
|
+
releaseWork = nil
|
|
258
|
+
targetCheckTimer?.invalidate()
|
|
259
|
+
targetCheckTimer = nil
|
|
260
|
+
targetPid = nil
|
|
261
|
+
targetWindowNumber = nil
|
|
262
|
+
targetWindowFrame = nil
|
|
263
|
+
cursorView.pressed = false
|
|
264
|
+
window.orderOut(nil)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
func stop() {
|
|
268
|
+
hide()
|
|
269
|
+
NSApp.stop(nil)
|
|
270
|
+
if let wakeEvent = NSEvent.otherEvent(
|
|
271
|
+
with: .applicationDefined,
|
|
272
|
+
location: .zero,
|
|
273
|
+
modifierFlags: [],
|
|
274
|
+
timestamp: ProcessInfo.processInfo.systemUptime,
|
|
275
|
+
windowNumber: 0,
|
|
276
|
+
context: nil,
|
|
277
|
+
subtype: 0,
|
|
278
|
+
data1: 0,
|
|
279
|
+
data2: 0
|
|
280
|
+
) {
|
|
281
|
+
NSApp.postEvent(wakeEvent, atStart: true)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private func scheduleHide(after milliseconds: Int) {
|
|
286
|
+
hideWork?.cancel()
|
|
287
|
+
guard milliseconds > 0 else { return }
|
|
288
|
+
let work = DispatchWorkItem { [weak self] in self?.hide() }
|
|
289
|
+
hideWork = work
|
|
290
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(milliseconds), execute: work)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private func scheduleTargetChecks() {
|
|
294
|
+
guard targetCheckTimer == nil else { return }
|
|
295
|
+
targetCheckTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] _ in
|
|
296
|
+
Task { @MainActor [weak self] in
|
|
297
|
+
guard let self, self.window.isVisible else { return }
|
|
298
|
+
if !self.targetWindowIsCurrent(
|
|
299
|
+
pid: self.targetPid,
|
|
300
|
+
windowNumber: self.targetWindowNumber,
|
|
301
|
+
expectedFrame: self.targetWindowFrame
|
|
302
|
+
) {
|
|
303
|
+
self.hide()
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private func appKitPoint(fromQuartz point: CGPoint) -> NSPoint {
|
|
310
|
+
for screen in NSScreen.screens {
|
|
311
|
+
guard let screenNumber = (screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value else {
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
let quartzFrame = CGDisplayBounds(CGDirectDisplayID(screenNumber))
|
|
315
|
+
if quartzFrame.contains(point) {
|
|
316
|
+
return NSPoint(
|
|
317
|
+
x: screen.frame.origin.x + (point.x - quartzFrame.origin.x),
|
|
318
|
+
y: screen.frame.maxY - (point.y - quartzFrame.origin.y)
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
let mainDisplay = CGMainDisplayID()
|
|
323
|
+
let quartzFrame = CGDisplayBounds(mainDisplay)
|
|
324
|
+
let screen = NSScreen.screens.first { candidate in
|
|
325
|
+
(candidate.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value == mainDisplay
|
|
326
|
+
} ?? NSScreen.main
|
|
327
|
+
let appKitFrame = screen?.frame ?? .zero
|
|
328
|
+
return NSPoint(
|
|
329
|
+
x: appKitFrame.origin.x + (point.x - quartzFrame.origin.x),
|
|
330
|
+
y: appKitFrame.maxY - (point.y - quartzFrame.origin.y)
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private func targetWindowIsCurrent(pid: pid_t?, windowNumber: Int64?, expectedFrame: CGRect?) -> Bool {
|
|
335
|
+
guard let pid, let windowNumber, let expectedFrame else { return false }
|
|
336
|
+
guard let rawWindows = CGWindowListCopyWindowInfo(
|
|
337
|
+
[.optionOnScreenOnly, .excludeDesktopElements],
|
|
338
|
+
kCGNullWindowID
|
|
339
|
+
) as? [[String: Any]] else {
|
|
340
|
+
return false
|
|
341
|
+
}
|
|
342
|
+
return rawWindows.contains { window in
|
|
343
|
+
guard (window[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value == pid,
|
|
344
|
+
(window[kCGWindowNumber as String] as? NSNumber)?.int64Value == windowNumber,
|
|
345
|
+
let bounds = window[kCGWindowBounds as String] as? [String: Any],
|
|
346
|
+
let currentFrame = CGRect(dictionaryRepresentation: bounds as CFDictionary) else { return false }
|
|
347
|
+
let tolerance: CGFloat = 2
|
|
348
|
+
return abs(currentFrame.minX - expectedFrame.minX) <= tolerance
|
|
349
|
+
&& abs(currentFrame.minY - expectedFrame.minY) <= tolerance
|
|
350
|
+
&& abs(currentFrame.width - expectedFrame.width) <= tolerance
|
|
351
|
+
&& abs(currentFrame.height - expectedFrame.height) <= tolerance
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@MainActor
|
|
357
|
+
enum CursorOverlayRuntime {
|
|
358
|
+
static func run() {
|
|
359
|
+
let app = NSApplication.shared
|
|
360
|
+
app.setActivationPolicy(.prohibited)
|
|
361
|
+
let controller = CursorOverlayController()
|
|
362
|
+
let input = FileHandle.standardInput
|
|
363
|
+
let parser = CursorCommandParser { object in
|
|
364
|
+
DispatchQueue.main.async {
|
|
365
|
+
handleCursorCommand(object, controller: controller)
|
|
366
|
+
}
|
|
367
|
+
} onEnd: {
|
|
368
|
+
DispatchQueue.main.async { controller.stop() }
|
|
369
|
+
} onInvalid: { message in
|
|
370
|
+
DispatchQueue.main.async {
|
|
371
|
+
emitCursorResponse(["ok": false, "error": message])
|
|
372
|
+
controller.stop()
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
input.readabilityHandler = { handle in parser.consume(handle.availableData) }
|
|
376
|
+
emitCursorResponse(["ok": true, "ready": true, "pid": ProcessInfo.processInfo.processIdentifier])
|
|
377
|
+
app.run()
|
|
378
|
+
input.readabilityHandler = nil
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private final class CursorCommandParser: @unchecked Sendable {
|
|
383
|
+
private static let maximumLineBytes = 16 * 1024
|
|
384
|
+
private let queue = DispatchQueue(label: "dsh-computer-use.cursor-protocol")
|
|
385
|
+
private let onObject: @Sendable ([String: Any]) -> Void
|
|
386
|
+
private let onEnd: @Sendable () -> Void
|
|
387
|
+
private let onInvalid: @Sendable (String) -> Void
|
|
388
|
+
private var buffer = Data()
|
|
389
|
+
private var ended = false
|
|
390
|
+
|
|
391
|
+
init(
|
|
392
|
+
onObject: @escaping @Sendable ([String: Any]) -> Void,
|
|
393
|
+
onEnd: @escaping @Sendable () -> Void,
|
|
394
|
+
onInvalid: @escaping @Sendable (String) -> Void
|
|
395
|
+
) {
|
|
396
|
+
self.onObject = onObject
|
|
397
|
+
self.onEnd = onEnd
|
|
398
|
+
self.onInvalid = onInvalid
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
func consume(_ data: Data) {
|
|
402
|
+
queue.async { [self] in
|
|
403
|
+
guard !ended, !data.isEmpty else {
|
|
404
|
+
guard !ended else { return }
|
|
405
|
+
ended = true
|
|
406
|
+
onEnd()
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
buffer.append(data)
|
|
410
|
+
if buffer.count > Self.maximumLineBytes, buffer.firstIndex(of: 0x0a) == nil {
|
|
411
|
+
ended = true
|
|
412
|
+
buffer.removeAll()
|
|
413
|
+
onInvalid("cursor overlay command exceeded the protocol limit")
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
while let newline = buffer.firstIndex(of: 0x0a) {
|
|
417
|
+
let line = buffer[..<newline]
|
|
418
|
+
buffer.removeSubrange(...newline)
|
|
419
|
+
if line.count > Self.maximumLineBytes {
|
|
420
|
+
ended = true
|
|
421
|
+
onInvalid("cursor overlay command exceeded the protocol limit")
|
|
422
|
+
return
|
|
423
|
+
}
|
|
424
|
+
guard !line.isEmpty else { continue }
|
|
425
|
+
guard let object = try? JSONSerialization.jsonObject(with: Data(line)) as? [String: Any] else {
|
|
426
|
+
ended = true
|
|
427
|
+
onInvalid("cursor overlay command is not valid JSON")
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
onObject(object)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
@MainActor
|
|
437
|
+
private func handleCursorCommand(_ object: [String: Any], controller: CursorOverlayController) {
|
|
438
|
+
do {
|
|
439
|
+
let command = try CursorOverlayCommand(object)
|
|
440
|
+
switch command.operation {
|
|
441
|
+
case "show", "move":
|
|
442
|
+
guard let point = command.point else {
|
|
443
|
+
throw CursorOverlayError(message: "cursor overlay command needs x and y")
|
|
444
|
+
}
|
|
445
|
+
controller.show(
|
|
446
|
+
at: point,
|
|
447
|
+
durationMs: command.durationMs,
|
|
448
|
+
autoHideMs: command.autoHideMs,
|
|
449
|
+
targetPid: command.targetPid,
|
|
450
|
+
targetWindowNumber: command.targetWindowNumber,
|
|
451
|
+
targetWindowFrame: command.targetWindowFrame
|
|
452
|
+
)
|
|
453
|
+
case "press":
|
|
454
|
+
controller.validateTarget(
|
|
455
|
+
pid: command.targetPid,
|
|
456
|
+
windowNumber: command.targetWindowNumber,
|
|
457
|
+
expectedFrame: command.targetWindowFrame
|
|
458
|
+
)
|
|
459
|
+
controller.press(autoHideMs: command.autoHideMs, sustained: command.sustainedPress)
|
|
460
|
+
case "release":
|
|
461
|
+
controller.validateTarget(
|
|
462
|
+
pid: command.targetPid,
|
|
463
|
+
windowNumber: command.targetWindowNumber,
|
|
464
|
+
expectedFrame: command.targetWindowFrame
|
|
465
|
+
)
|
|
466
|
+
controller.release(autoHideMs: command.autoHideMs)
|
|
467
|
+
case "hide":
|
|
468
|
+
controller.hide()
|
|
469
|
+
case "stop":
|
|
470
|
+
controller.stop()
|
|
471
|
+
case "ping":
|
|
472
|
+
break
|
|
473
|
+
default:
|
|
474
|
+
throw CursorOverlayError(message: "unknown cursor overlay operation")
|
|
475
|
+
}
|
|
476
|
+
emitCursorResponse(["ok": true, "op": command.operation])
|
|
477
|
+
} catch let error as CursorOverlayError {
|
|
478
|
+
emitCursorResponse(["ok": false, "error": error.message])
|
|
479
|
+
} catch {
|
|
480
|
+
emitCursorResponse(["ok": false, "error": String(describing: error)])
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private func emitCursorResponse(_ payload: [String: Any]) {
|
|
485
|
+
guard let data = try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]) else { return }
|
|
486
|
+
FileHandle.standardOutput.write(data)
|
|
487
|
+
FileHandle.standardOutput.write(Data("\n".utf8))
|
|
488
|
+
}
|