@anionex/dsh-computer-use 0.2.0 → 0.3.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/README.md +13 -7
- package/README.zh.md +13 -7
- package/cordis.patch.yml +3 -0
- package/docs/interaction-policy.md +6 -4
- package/docs/interaction-policy.zh.md +7 -5
- package/lib/client.js +23 -11
- package/lib/client.js.map +1 -1
- package/lib/config.js +13 -3
- package/lib/config.js.map +1 -1
- package/lib/providers/macos.js +79 -14
- package/lib/providers/macos.js.map +1 -1
- package/lib/providers/native-helper.js +247 -22
- package/lib/providers/native-helper.js.map +1 -1
- package/lib/providers/unsupported.js +6 -0
- package/lib/providers/unsupported.js.map +1 -1
- package/lib/service.js +74 -32
- package/lib/service.js.map +1 -1
- package/lib/types/backend.d.ts +18 -3
- package/lib/types/backend.d.ts.map +1 -1
- package/lib/types/client/index.d.ts +5 -2
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/settings-validation.d.ts +3 -0
- package/lib/types/client/settings-validation.d.ts.map +1 -0
- package/lib/types/config.d.ts +7 -1
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/providers/macos.d.ts +8 -2
- package/lib/types/providers/macos.d.ts.map +1 -1
- package/lib/types/providers/native-helper.d.ts +9 -1
- package/lib/types/providers/native-helper.d.ts.map +1 -1
- package/lib/types/providers/unsupported.d.ts +6 -2
- package/lib/types/providers/unsupported.d.ts.map +1 -1
- package/lib/types/service.d.ts.map +1 -1
- package/native/macos/Sources/Fixture/main.swift +46 -0
- package/native/macos/Sources/Helper/CursorMotion.swift +67 -0
- package/native/macos/Sources/Helper/CursorOverlay.swift +334 -60
- package/native/macos/Sources/Helper/TargetedPointer.swift +23 -10
- package/native/macos/Sources/Helper/main.swift +80 -5
- package/native/macos/Sources/Monitor/main.swift +15 -1
- package/native/macos/bin/dsh-computer-use-helper +0 -0
- package/native/macos/manifest.json +3 -3
- package/package.json +16 -18
- package/scripts/build-client.mjs +0 -0
- package/scripts/build-native.mjs +0 -0
- package/scripts/model-e2e.mjs +0 -0
- package/scripts/validate.mjs +0 -0
- package/src/backend.ts +18 -3
- package/src/client/index.tsx +36 -13
- package/src/client/settings-validation.ts +14 -0
- package/src/config.ts +20 -4
- package/src/providers/macos.ts +102 -14
- package/src/providers/native-helper.ts +241 -22
- package/src/providers/unsupported.ts +11 -1
- package/src/service.ts +100 -37
|
@@ -7,7 +7,9 @@ import QuartzCore
|
|
|
7
7
|
private struct CursorOverlayCommand {
|
|
8
8
|
let operation: String
|
|
9
9
|
let point: CGPoint?
|
|
10
|
-
let durationMs: Int
|
|
10
|
+
let durationMs: Int?
|
|
11
|
+
let speedPxPerSecond: Double?
|
|
12
|
+
let accelerationPxPerSecondSquared: Double?
|
|
11
13
|
let autoHideMs: Int
|
|
12
14
|
let targetPid: pid_t?
|
|
13
15
|
let targetWindowNumber: Int64?
|
|
@@ -16,6 +18,8 @@ private struct CursorOverlayCommand {
|
|
|
16
18
|
|
|
17
19
|
private static let maximumCoordinateMagnitude = 100_000.0
|
|
18
20
|
private static let maximumDurationMs = 2_000
|
|
21
|
+
private static let maximumSpeedPxPerSecond = 50_000.0
|
|
22
|
+
private static let maximumAccelerationPxPerSecondSquared = 500_000.0
|
|
19
23
|
private static let maximumAutoHideMs = 30_000
|
|
20
24
|
|
|
21
25
|
private static func number(_ object: Any?, field: String) throws -> NSNumber? {
|
|
@@ -56,14 +60,31 @@ private struct CursorOverlayCommand {
|
|
|
56
60
|
} else {
|
|
57
61
|
self.point = nil
|
|
58
62
|
}
|
|
59
|
-
let durationMs =
|
|
63
|
+
let durationMs = object["durationMs"] == nil
|
|
64
|
+
? nil
|
|
65
|
+
: try Self.integer(object["durationMs"], field: "durationMs", fallback: 180)
|
|
60
66
|
let autoHideMs = try Self.integer(object["autoHideMs"], field: "autoHideMs", fallback: 0)
|
|
61
|
-
guard durationMs >= 0
|
|
67
|
+
guard durationMs == nil || (durationMs! >= 0 && durationMs! <= Self.maximumDurationMs),
|
|
62
68
|
autoHideMs >= 0, autoHideMs <= Self.maximumAutoHideMs else {
|
|
63
69
|
throw CursorOverlayError(message: "cursor overlay timing is outside the supported range")
|
|
64
70
|
}
|
|
65
71
|
self.durationMs = durationMs
|
|
66
72
|
self.autoHideMs = autoHideMs
|
|
73
|
+
let speed = try Self.number(object["speedPxPerSecond"], field: "speedPxPerSecond")?.doubleValue
|
|
74
|
+
let acceleration = try Self.number(
|
|
75
|
+
object["accelerationPxPerSecondSquared"],
|
|
76
|
+
field: "accelerationPxPerSecondSquared"
|
|
77
|
+
)?.doubleValue
|
|
78
|
+
guard (speed == nil) == (acceleration == nil) else {
|
|
79
|
+
throw CursorOverlayError(message: "cursor overlay speed and acceleration must be provided together")
|
|
80
|
+
}
|
|
81
|
+
guard speed == nil || (speed!.isFinite && speed! >= 100 && speed! <= Self.maximumSpeedPxPerSecond),
|
|
82
|
+
acceleration == nil || (acceleration!.isFinite && acceleration! >= 100
|
|
83
|
+
&& acceleration! <= Self.maximumAccelerationPxPerSecondSquared) else {
|
|
84
|
+
throw CursorOverlayError(message: "cursor overlay physical motion is outside the supported range")
|
|
85
|
+
}
|
|
86
|
+
self.speedPxPerSecond = speed
|
|
87
|
+
self.accelerationPxPerSecondSquared = acceleration
|
|
67
88
|
|
|
68
89
|
if let rawPid = try Self.number(object["targetPid"], field: "targetPid") {
|
|
69
90
|
let pidValue = rawPid.doubleValue
|
|
@@ -162,6 +183,9 @@ private final class CursorOverlayController: NSObject {
|
|
|
162
183
|
private var releaseWork: DispatchWorkItem?
|
|
163
184
|
private var targetCheckTimer: Timer?
|
|
164
185
|
private var glideTimer: Timer?
|
|
186
|
+
private var glideCompletion: ((Bool) -> Void)?
|
|
187
|
+
private var hasPosition = false
|
|
188
|
+
private var currentQuartzPoint: CGPoint?
|
|
165
189
|
private var targetPid: pid_t?
|
|
166
190
|
private var targetWindowNumber: Int64?
|
|
167
191
|
private var targetWindowFrame: CGRect?
|
|
@@ -187,6 +211,16 @@ private final class CursorOverlayController: NSObject {
|
|
|
187
211
|
window.collectionBehavior = [.fullScreenAuxiliary, .stationary, .ignoresCycle]
|
|
188
212
|
window.isReleasedWhenClosed = false
|
|
189
213
|
window.sharingType = .readOnly
|
|
214
|
+
NSWorkspace.shared.notificationCenter.addObserver(
|
|
215
|
+
self,
|
|
216
|
+
selector: #selector(frontmostApplicationChanged(_:)),
|
|
217
|
+
name: NSWorkspace.didActivateApplicationNotification,
|
|
218
|
+
object: nil
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
deinit {
|
|
223
|
+
NSWorkspace.shared.notificationCenter.removeObserver(self)
|
|
190
224
|
}
|
|
191
225
|
|
|
192
226
|
/// Outcome of a placement request, so the caller can distinguish a cursor
|
|
@@ -195,35 +229,69 @@ private final class CursorOverlayController: NSObject {
|
|
|
195
229
|
/// ends up with a frozen agent cursor and no error anywhere.
|
|
196
230
|
enum Placement {
|
|
197
231
|
case shown
|
|
232
|
+
case targetNotFrontmost
|
|
198
233
|
case targetUnavailable
|
|
199
234
|
}
|
|
200
235
|
|
|
201
|
-
@discardableResult
|
|
202
236
|
func show(
|
|
203
237
|
at quartzPoint: CGPoint,
|
|
204
|
-
durationMs: Int
|
|
238
|
+
durationMs: Int?,
|
|
239
|
+
speedPxPerSecond: Double?,
|
|
240
|
+
accelerationPxPerSecondSquared: Double?,
|
|
205
241
|
autoHideMs: Int,
|
|
206
242
|
targetPid: pid_t?,
|
|
207
243
|
targetWindowNumber: Int64?,
|
|
208
|
-
targetWindowFrame: CGRect
|
|
209
|
-
|
|
244
|
+
targetWindowFrame: CGRect?,
|
|
245
|
+
completion: @escaping (Placement) -> Void
|
|
246
|
+
) {
|
|
210
247
|
hideWork?.cancel()
|
|
211
|
-
|
|
248
|
+
let placement = targetPlacement(pid: targetPid, windowNumber: targetWindowNumber, expectedFrame: targetWindowFrame)
|
|
249
|
+
guard placement == .shown else {
|
|
212
250
|
hide()
|
|
213
|
-
|
|
251
|
+
completion(placement)
|
|
252
|
+
return
|
|
214
253
|
}
|
|
215
254
|
self.targetPid = targetPid
|
|
216
255
|
self.targetWindowNumber = targetWindowNumber
|
|
217
256
|
self.targetWindowFrame = targetWindowFrame
|
|
218
257
|
scheduleTargetChecks()
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
x:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
258
|
+
if !hasPosition {
|
|
259
|
+
let mouse = NSEvent.mouseLocation
|
|
260
|
+
window.setFrameOrigin(NSPoint(x: mouse.x, y: mouse.y - Self.size.height))
|
|
261
|
+
currentQuartzPoint = self.quartzPoint(fromAppKit: mouse)
|
|
262
|
+
hasPosition = true
|
|
263
|
+
}
|
|
264
|
+
window.orderFrontRegardless()
|
|
265
|
+
let shownPlacement = targetPlacement(pid: targetPid, windowNumber: targetWindowNumber, expectedFrame: targetWindowFrame)
|
|
266
|
+
guard shownPlacement == .shown else {
|
|
267
|
+
hide()
|
|
268
|
+
completion(shownPlacement)
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
glide(
|
|
272
|
+
from: currentQuartzPoint ?? quartzPoint,
|
|
273
|
+
to: quartzPoint,
|
|
274
|
+
durationMs: durationMs,
|
|
275
|
+
speedPxPerSecond: speedPxPerSecond,
|
|
276
|
+
accelerationPxPerSecondSquared: accelerationPxPerSecondSquared
|
|
277
|
+
) { [weak self] reached in
|
|
278
|
+
guard let self else {
|
|
279
|
+
completion(.targetUnavailable)
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
let finalPlacement = self.targetPlacement(
|
|
283
|
+
pid: targetPid,
|
|
284
|
+
windowNumber: targetWindowNumber,
|
|
285
|
+
expectedFrame: targetWindowFrame
|
|
286
|
+
)
|
|
287
|
+
guard reached, finalPlacement == .shown else {
|
|
288
|
+
self.hide()
|
|
289
|
+
completion(finalPlacement == .shown ? .targetUnavailable : finalPlacement)
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
self.scheduleHide(after: autoHideMs)
|
|
293
|
+
completion(.shown)
|
|
294
|
+
}
|
|
227
295
|
}
|
|
228
296
|
|
|
229
297
|
/// Travel to `targetOrigin`, interpolated on the main run loop.
|
|
@@ -237,39 +305,98 @@ private final class CursorOverlayController: NSObject {
|
|
|
237
305
|
///
|
|
238
306
|
/// Stepping the origin directly is the same call the first placement
|
|
239
307
|
/// already proved works, so the glide is both visible and correct.
|
|
240
|
-
private func glide(
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
308
|
+
private func glide(
|
|
309
|
+
from origin: CGPoint,
|
|
310
|
+
to target: CGPoint,
|
|
311
|
+
durationMs: Int?,
|
|
312
|
+
speedPxPerSecond: Double?,
|
|
313
|
+
accelerationPxPerSecondSquared: Double?,
|
|
314
|
+
completion: @escaping (Bool) -> Void
|
|
315
|
+
) {
|
|
316
|
+
finishGlide(reached: false)
|
|
317
|
+
guard origin != target else {
|
|
318
|
+
currentQuartzPoint = target
|
|
319
|
+
window.setFrameOrigin(panelOrigin(fromQuartz: target))
|
|
320
|
+
window.orderFrontRegardless()
|
|
321
|
+
completion(true)
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
let distance = hypot(target.x - origin.x, target.y - origin.y)
|
|
325
|
+
let duration = motionDuration(
|
|
326
|
+
distance: distance,
|
|
327
|
+
explicitDurationMs: durationMs,
|
|
328
|
+
speedPxPerSecond: speedPxPerSecond,
|
|
329
|
+
accelerationPxPerSecondSquared: accelerationPxPerSecondSquared
|
|
330
|
+
)
|
|
331
|
+
guard duration > 0 else {
|
|
332
|
+
currentQuartzPoint = target
|
|
333
|
+
window.setFrameOrigin(panelOrigin(fromQuartz: target))
|
|
246
334
|
window.orderFrontRegardless()
|
|
335
|
+
completion(true)
|
|
247
336
|
return
|
|
248
337
|
}
|
|
249
|
-
let started =
|
|
250
|
-
|
|
251
|
-
let timer = Timer(timeInterval: 1.0 /
|
|
338
|
+
let started = CACurrentMediaTime()
|
|
339
|
+
glideCompletion = completion
|
|
340
|
+
let timer = Timer(timeInterval: 1.0 / 120.0, repeats: true) { [weak self] timer in
|
|
252
341
|
MainActor.assumeIsolated {
|
|
253
342
|
guard let self else { timer.invalidate(); return }
|
|
254
|
-
let
|
|
343
|
+
if let targetPid = self.targetPid,
|
|
344
|
+
NSWorkspace.shared.frontmostApplication?.processIdentifier != targetPid {
|
|
345
|
+
self.hide()
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
let elapsed = CACurrentMediaTime() - started
|
|
255
349
|
if elapsed >= duration {
|
|
256
|
-
|
|
257
|
-
self.
|
|
258
|
-
self.
|
|
350
|
+
self.currentQuartzPoint = target
|
|
351
|
+
self.window.setFrameOrigin(self.panelOrigin(fromQuartz: target))
|
|
352
|
+
self.hasPosition = true
|
|
353
|
+
self.finishGlide(reached: true)
|
|
259
354
|
return
|
|
260
355
|
}
|
|
261
|
-
|
|
262
|
-
let fraction
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
356
|
+
let linear = elapsed / duration
|
|
357
|
+
let fraction: Double
|
|
358
|
+
if let speedPxPerSecond, let accelerationPxPerSecondSquared {
|
|
359
|
+
fraction = cursorMotionFraction(
|
|
360
|
+
progress: linear,
|
|
361
|
+
distance: distance,
|
|
362
|
+
speed: speedPxPerSecond,
|
|
363
|
+
acceleration: accelerationPxPerSecondSquared
|
|
364
|
+
)
|
|
365
|
+
} else {
|
|
366
|
+
fraction = linear * linear * (3 - 2 * linear)
|
|
367
|
+
}
|
|
368
|
+
let point = cursorMotionPoint(from: origin, to: target, fraction: fraction)
|
|
369
|
+
self.currentQuartzPoint = point
|
|
370
|
+
self.window.setFrameOrigin(self.panelOrigin(fromQuartz: point))
|
|
267
371
|
}
|
|
268
372
|
}
|
|
269
373
|
glideTimer = timer
|
|
270
374
|
RunLoop.main.add(timer, forMode: .common)
|
|
271
375
|
}
|
|
272
376
|
|
|
377
|
+
private func motionDuration(
|
|
378
|
+
distance: Double,
|
|
379
|
+
explicitDurationMs: Int?,
|
|
380
|
+
speedPxPerSecond: Double?,
|
|
381
|
+
accelerationPxPerSecondSquared: Double?
|
|
382
|
+
) -> Double {
|
|
383
|
+
if let explicitDurationMs { return Double(explicitDurationMs) / 1000 }
|
|
384
|
+
guard let speedPxPerSecond, let accelerationPxPerSecondSquared else { return 0.18 }
|
|
385
|
+
return cursorMotionDuration(
|
|
386
|
+
distance: distance,
|
|
387
|
+
speed: speedPxPerSecond,
|
|
388
|
+
acceleration: accelerationPxPerSecondSquared
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private func finishGlide(reached: Bool) {
|
|
393
|
+
glideTimer?.invalidate()
|
|
394
|
+
glideTimer = nil
|
|
395
|
+
let completion = glideCompletion
|
|
396
|
+
glideCompletion = nil
|
|
397
|
+
completion?(reached)
|
|
398
|
+
}
|
|
399
|
+
|
|
273
400
|
func press(autoHideMs: Int, sustained: Bool) {
|
|
274
401
|
guard window.isVisible else { return }
|
|
275
402
|
releaseWork?.cancel()
|
|
@@ -297,16 +424,16 @@ private final class CursorOverlayController: NSObject {
|
|
|
297
424
|
@discardableResult
|
|
298
425
|
func validateTarget(pid: pid_t?, windowNumber: Int64?, expectedFrame: CGRect?) -> Placement {
|
|
299
426
|
guard window.isVisible else { return .targetUnavailable }
|
|
300
|
-
|
|
427
|
+
let placement = targetPlacement(pid: pid, windowNumber: windowNumber, expectedFrame: expectedFrame)
|
|
428
|
+
if placement != .shown {
|
|
301
429
|
hide()
|
|
302
|
-
return
|
|
430
|
+
return placement
|
|
303
431
|
}
|
|
304
432
|
return .shown
|
|
305
433
|
}
|
|
306
434
|
|
|
307
435
|
func hide() {
|
|
308
|
-
|
|
309
|
-
glideTimer = nil
|
|
436
|
+
finishGlide(reached: false)
|
|
310
437
|
hideWork?.cancel()
|
|
311
438
|
hideWork = nil
|
|
312
439
|
releaseWork?.cancel()
|
|
@@ -351,17 +478,24 @@ private final class CursorOverlayController: NSObject {
|
|
|
351
478
|
targetCheckTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] _ in
|
|
352
479
|
Task { @MainActor [weak self] in
|
|
353
480
|
guard let self, self.window.isVisible else { return }
|
|
354
|
-
if
|
|
481
|
+
if self.targetPlacement(
|
|
355
482
|
pid: self.targetPid,
|
|
356
483
|
windowNumber: self.targetWindowNumber,
|
|
357
484
|
expectedFrame: self.targetWindowFrame
|
|
358
|
-
) {
|
|
485
|
+
) != .shown {
|
|
359
486
|
self.hide()
|
|
360
487
|
}
|
|
361
488
|
}
|
|
362
489
|
}
|
|
363
490
|
}
|
|
364
491
|
|
|
492
|
+
@objc private func frontmostApplicationChanged(_ notification: Notification) {
|
|
493
|
+
guard window.isVisible, let targetPid else { return }
|
|
494
|
+
if NSWorkspace.shared.frontmostApplication?.processIdentifier != targetPid {
|
|
495
|
+
hide()
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
365
499
|
private func appKitPoint(fromQuartz point: CGPoint) -> NSPoint {
|
|
366
500
|
for screen in NSScreen.screens {
|
|
367
501
|
guard let screenNumber = (screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value else {
|
|
@@ -387,15 +521,47 @@ private final class CursorOverlayController: NSObject {
|
|
|
387
521
|
)
|
|
388
522
|
}
|
|
389
523
|
|
|
390
|
-
private func
|
|
391
|
-
|
|
524
|
+
private func quartzPoint(fromAppKit point: NSPoint) -> CGPoint {
|
|
525
|
+
for screen in NSScreen.screens {
|
|
526
|
+
guard screen.frame.contains(point),
|
|
527
|
+
let screenNumber = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else {
|
|
528
|
+
continue
|
|
529
|
+
}
|
|
530
|
+
let quartzFrame = CGDisplayBounds(CGDirectDisplayID(screenNumber.uint32Value))
|
|
531
|
+
return CGPoint(
|
|
532
|
+
x: quartzFrame.origin.x + (point.x - screen.frame.origin.x),
|
|
533
|
+
y: quartzFrame.origin.y + (screen.frame.maxY - point.y)
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
let mainDisplay = CGMainDisplayID()
|
|
537
|
+
let quartzFrame = CGDisplayBounds(mainDisplay)
|
|
538
|
+
let appKitFrame = NSScreen.main?.frame ?? NSRect(
|
|
539
|
+
x: quartzFrame.origin.x,
|
|
540
|
+
y: quartzFrame.origin.y,
|
|
541
|
+
width: quartzFrame.width,
|
|
542
|
+
height: quartzFrame.height
|
|
543
|
+
)
|
|
544
|
+
return CGPoint(
|
|
545
|
+
x: quartzFrame.origin.x + (point.x - appKitFrame.origin.x),
|
|
546
|
+
y: quartzFrame.origin.y + (appKitFrame.maxY - point.y)
|
|
547
|
+
)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private func panelOrigin(fromQuartz point: CGPoint) -> NSPoint {
|
|
551
|
+
let appKit = appKitPoint(fromQuartz: point)
|
|
552
|
+
return NSPoint(x: appKit.x, y: appKit.y - Self.size.height)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private func targetPlacement(pid: pid_t?, windowNumber: Int64?, expectedFrame: CGRect?) -> Placement {
|
|
556
|
+
guard let pid, let windowNumber, let expectedFrame else { return .targetUnavailable }
|
|
557
|
+
guard NSWorkspace.shared.frontmostApplication?.processIdentifier == pid else { return .targetNotFrontmost }
|
|
392
558
|
guard let rawWindows = CGWindowListCopyWindowInfo(
|
|
393
559
|
[.optionOnScreenOnly, .excludeDesktopElements],
|
|
394
560
|
kCGNullWindowID
|
|
395
561
|
) as? [[String: Any]] else {
|
|
396
|
-
return
|
|
562
|
+
return .targetUnavailable
|
|
397
563
|
}
|
|
398
|
-
|
|
564
|
+
let matches = rawWindows.contains { window in
|
|
399
565
|
guard (window[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value == pid,
|
|
400
566
|
(window[kCGWindowNumber as String] as? NSNumber)?.int64Value == windowNumber,
|
|
401
567
|
let bounds = window[kCGWindowBounds as String] as? [String: Any],
|
|
@@ -406,6 +572,7 @@ private final class CursorOverlayController: NSObject {
|
|
|
406
572
|
&& abs(currentFrame.width - expectedFrame.width) <= tolerance
|
|
407
573
|
&& abs(currentFrame.height - expectedFrame.height) <= tolerance
|
|
408
574
|
}
|
|
575
|
+
return matches ? .shown : .targetUnavailable
|
|
409
576
|
}
|
|
410
577
|
}
|
|
411
578
|
|
|
@@ -415,17 +582,17 @@ enum CursorOverlayRuntime {
|
|
|
415
582
|
let app = NSApplication.shared
|
|
416
583
|
app.setActivationPolicy(.prohibited)
|
|
417
584
|
let controller = CursorOverlayController()
|
|
585
|
+
let dispatcher = CursorCommandDispatcher(controller: controller)
|
|
418
586
|
let input = FileHandle.standardInput
|
|
419
587
|
let parser = CursorCommandParser { object in
|
|
420
588
|
DispatchQueue.main.async {
|
|
421
|
-
|
|
589
|
+
dispatcher.enqueue(object)
|
|
422
590
|
}
|
|
423
591
|
} onEnd: {
|
|
424
|
-
DispatchQueue.main.async {
|
|
592
|
+
DispatchQueue.main.async { dispatcher.endInput() }
|
|
425
593
|
} onInvalid: { message in
|
|
426
594
|
DispatchQueue.main.async {
|
|
427
|
-
|
|
428
|
-
controller.stop()
|
|
595
|
+
dispatcher.invalidate(message)
|
|
429
596
|
}
|
|
430
597
|
}
|
|
431
598
|
input.readabilityHandler = { handle in parser.consume(handle.availableData) }
|
|
@@ -435,6 +602,68 @@ enum CursorOverlayRuntime {
|
|
|
435
602
|
}
|
|
436
603
|
}
|
|
437
604
|
|
|
605
|
+
@MainActor
|
|
606
|
+
private final class CursorCommandDispatcher {
|
|
607
|
+
private let controller: CursorOverlayController
|
|
608
|
+
private var commands: [[String: Any]] = []
|
|
609
|
+
private var processing = false
|
|
610
|
+
private var inputEnded = false
|
|
611
|
+
private var terminated = false
|
|
612
|
+
|
|
613
|
+
init(controller: CursorOverlayController) {
|
|
614
|
+
self.controller = controller
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
func enqueue(_ object: [String: Any]) {
|
|
618
|
+
guard !inputEnded, !terminated else { return }
|
|
619
|
+
commands.append(object)
|
|
620
|
+
if object["op"] as? String == "stop", processing {
|
|
621
|
+
controller.hide()
|
|
622
|
+
}
|
|
623
|
+
drain()
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
func endInput() {
|
|
627
|
+
guard !inputEnded, !terminated else { return }
|
|
628
|
+
inputEnded = true
|
|
629
|
+
if commands.contains(where: { $0["op"] as? String == "stop" }) {
|
|
630
|
+
if processing { controller.hide() }
|
|
631
|
+
drain()
|
|
632
|
+
} else {
|
|
633
|
+
terminated = true
|
|
634
|
+
commands.removeAll()
|
|
635
|
+
controller.stop()
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
func invalidate(_ message: String) {
|
|
640
|
+
guard !terminated else { return }
|
|
641
|
+
inputEnded = true
|
|
642
|
+
terminated = true
|
|
643
|
+
commands.removeAll()
|
|
644
|
+
controller.hide()
|
|
645
|
+
emitCursorResponse(["ok": false, "error": message])
|
|
646
|
+
controller.stop()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private func drain() {
|
|
650
|
+
guard !terminated, !processing, !commands.isEmpty else { return }
|
|
651
|
+
processing = true
|
|
652
|
+
let command = commands.removeFirst()
|
|
653
|
+
let stopsRuntime = command["op"] as? String == "stop"
|
|
654
|
+
handleCursorCommand(command, controller: controller) { [weak self] in
|
|
655
|
+
guard let self else { return }
|
|
656
|
+
self.processing = false
|
|
657
|
+
if stopsRuntime {
|
|
658
|
+
self.terminated = true
|
|
659
|
+
self.commands.removeAll()
|
|
660
|
+
return
|
|
661
|
+
}
|
|
662
|
+
DispatchQueue.main.async { [weak self] in self?.drain() }
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
438
667
|
private final class CursorCommandParser: @unchecked Sendable {
|
|
439
668
|
private static let maximumLineBytes = 16 * 1024
|
|
440
669
|
private let queue = DispatchQueue(label: "dsh-computer-use.cursor-protocol")
|
|
@@ -490,7 +719,11 @@ private final class CursorCommandParser: @unchecked Sendable {
|
|
|
490
719
|
}
|
|
491
720
|
|
|
492
721
|
@MainActor
|
|
493
|
-
private func handleCursorCommand(
|
|
722
|
+
private func handleCursorCommand(
|
|
723
|
+
_ object: [String: Any],
|
|
724
|
+
controller: CursorOverlayController,
|
|
725
|
+
completion: @escaping () -> Void
|
|
726
|
+
) {
|
|
494
727
|
do {
|
|
495
728
|
let command = try CursorOverlayCommand(object)
|
|
496
729
|
// Whether the panel is actually on screen after this command. A hidden
|
|
@@ -498,39 +731,58 @@ private func handleCursorCommand(_ object: [String: Any], controller: CursorOver
|
|
|
498
731
|
// error -- but the caller must be able to tell, or the user silently
|
|
499
732
|
// loses sight of where the agent is acting.
|
|
500
733
|
var visible = true
|
|
734
|
+
var reasonCode: String?
|
|
501
735
|
switch command.operation {
|
|
502
736
|
case "show", "move":
|
|
503
737
|
guard let point = command.point else {
|
|
504
738
|
throw CursorOverlayError(message: "cursor overlay command needs x and y")
|
|
505
739
|
}
|
|
506
|
-
|
|
740
|
+
controller.show(
|
|
507
741
|
at: point,
|
|
508
742
|
durationMs: command.durationMs,
|
|
743
|
+
speedPxPerSecond: command.speedPxPerSecond,
|
|
744
|
+
accelerationPxPerSecondSquared: command.accelerationPxPerSecondSquared,
|
|
509
745
|
autoHideMs: command.autoHideMs,
|
|
510
746
|
targetPid: command.targetPid,
|
|
511
747
|
targetWindowNumber: command.targetWindowNumber,
|
|
512
748
|
targetWindowFrame: command.targetWindowFrame
|
|
513
|
-
)
|
|
749
|
+
) { placement in
|
|
750
|
+
let shown = placement == .shown
|
|
751
|
+
var response: [String: Any] = ["ok": true, "op": command.operation, "visible": shown]
|
|
752
|
+
if !shown {
|
|
753
|
+
response["reasonCode"] = cursorReasonCode(placement)
|
|
754
|
+
response["reason"] = cursorReason(placement)
|
|
755
|
+
}
|
|
756
|
+
emitCursorResponse(response)
|
|
757
|
+
completion()
|
|
758
|
+
}
|
|
759
|
+
return
|
|
514
760
|
case "press":
|
|
515
|
-
|
|
761
|
+
let placement = controller.validateTarget(
|
|
516
762
|
pid: command.targetPid,
|
|
517
763
|
windowNumber: command.targetWindowNumber,
|
|
518
764
|
expectedFrame: command.targetWindowFrame
|
|
519
|
-
)
|
|
765
|
+
)
|
|
766
|
+
visible = placement == .shown
|
|
767
|
+
reasonCode = cursorReasonCode(placement)
|
|
520
768
|
controller.press(autoHideMs: command.autoHideMs, sustained: command.sustainedPress)
|
|
521
769
|
case "release":
|
|
522
|
-
|
|
770
|
+
let placement = controller.validateTarget(
|
|
523
771
|
pid: command.targetPid,
|
|
524
772
|
windowNumber: command.targetWindowNumber,
|
|
525
773
|
expectedFrame: command.targetWindowFrame
|
|
526
|
-
)
|
|
774
|
+
)
|
|
775
|
+
visible = placement == .shown
|
|
776
|
+
reasonCode = cursorReasonCode(placement)
|
|
527
777
|
controller.release(autoHideMs: command.autoHideMs)
|
|
528
778
|
case "validate":
|
|
529
|
-
|
|
779
|
+
let placement = controller.validateTarget(
|
|
530
780
|
pid: command.targetPid,
|
|
531
781
|
windowNumber: command.targetWindowNumber,
|
|
532
782
|
expectedFrame: command.targetWindowFrame
|
|
533
|
-
)
|
|
783
|
+
)
|
|
784
|
+
visible = placement == .shown
|
|
785
|
+
reasonCode = cursorReasonCode(placement)
|
|
534
786
|
case "hide":
|
|
535
787
|
controller.hide()
|
|
536
788
|
visible = false
|
|
@@ -546,13 +798,35 @@ private func handleCursorCommand(_ object: [String: Any], controller: CursorOver
|
|
|
546
798
|
if !visible && (command.operation == "show" || command.operation == "move"
|
|
547
799
|
|| command.operation == "press" || command.operation == "release"
|
|
548
800
|
|| command.operation == "validate") {
|
|
549
|
-
response["
|
|
801
|
+
response["reasonCode"] = reasonCode
|
|
802
|
+
response["reason"] = reasonCode == "target-not-frontmost"
|
|
803
|
+
? "the bound target application is not frontmost; the agent cursor is hidden"
|
|
804
|
+
: "the bound target window no longer matches; the agent cursor is hidden"
|
|
550
805
|
}
|
|
551
806
|
emitCursorResponse(response)
|
|
807
|
+
completion()
|
|
552
808
|
} catch let error as CursorOverlayError {
|
|
553
809
|
emitCursorResponse(["ok": false, "error": error.message])
|
|
810
|
+
completion()
|
|
554
811
|
} catch {
|
|
555
812
|
emitCursorResponse(["ok": false, "error": String(describing: error)])
|
|
813
|
+
completion()
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
private func cursorReasonCode(_ placement: CursorOverlayController.Placement) -> String? {
|
|
818
|
+
switch placement {
|
|
819
|
+
case .shown: return nil
|
|
820
|
+
case .targetNotFrontmost: return "target-not-frontmost"
|
|
821
|
+
case .targetUnavailable: return "target-invalid"
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
private func cursorReason(_ placement: CursorOverlayController.Placement) -> String? {
|
|
826
|
+
switch placement {
|
|
827
|
+
case .shown: return nil
|
|
828
|
+
case .targetNotFrontmost: return "the bound target application is not frontmost; the agent cursor is hidden"
|
|
829
|
+
case .targetUnavailable: return "the bound target window no longer matches; the agent cursor is hidden"
|
|
556
830
|
}
|
|
557
831
|
}
|
|
558
832
|
|
|
@@ -246,7 +246,9 @@ func targetedScroll(
|
|
|
246
246
|
func targetedDrag(
|
|
247
247
|
from: CGPoint,
|
|
248
248
|
to: CGPoint,
|
|
249
|
-
target: TargetedPointerTarget
|
|
249
|
+
target: TargetedPointerTarget,
|
|
250
|
+
speedPxPerSecond: Double,
|
|
251
|
+
accelerationPxPerSecondSquared: Double
|
|
250
252
|
) throws {
|
|
251
253
|
let source = try targetedPointerSource()
|
|
252
254
|
let group = clickGroup()
|
|
@@ -261,7 +263,7 @@ func targetedDrag(
|
|
|
261
263
|
gesturePhase: 2,
|
|
262
264
|
clickGroup: group
|
|
263
265
|
)
|
|
264
|
-
usleep(
|
|
266
|
+
usleep(2_000)
|
|
265
267
|
|
|
266
268
|
let down = try pointerEvent(source: source, type: .leftMouseDown, button: .left)
|
|
267
269
|
down.setDoubleValueField(.mouseEventPressure, value: 1)
|
|
@@ -275,14 +277,25 @@ func targetedDrag(
|
|
|
275
277
|
gesturePhase: 3,
|
|
276
278
|
clickGroup: group
|
|
277
279
|
)
|
|
278
|
-
usleep(
|
|
280
|
+
usleep(2_000)
|
|
279
281
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
let distance = hypot(to.x - from.x, to.y - from.y)
|
|
283
|
+
let duration = distance == 0 ? 0 : cursorMotionDuration(
|
|
284
|
+
distance: distance,
|
|
285
|
+
speed: speedPxPerSecond,
|
|
286
|
+
acceleration: accelerationPxPerSecondSquared
|
|
287
|
+
)
|
|
288
|
+
let started = ProcessInfo.processInfo.systemUptime
|
|
289
|
+
while true {
|
|
290
|
+
let elapsed = ProcessInfo.processInfo.systemUptime - started
|
|
291
|
+
let progress = duration == 0 ? 1 : min(1, elapsed / duration)
|
|
292
|
+
let fraction = cursorMotionFraction(
|
|
293
|
+
progress: progress,
|
|
294
|
+
distance: distance,
|
|
295
|
+
speed: speedPxPerSecond,
|
|
296
|
+
acceleration: accelerationPxPerSecondSquared
|
|
285
297
|
)
|
|
298
|
+
let point = cursorMotionPoint(from: from, to: to, fraction: fraction)
|
|
286
299
|
let dragged = try pointerEvent(source: source, type: .leftMouseDragged, button: .left)
|
|
287
300
|
dragged.setDoubleValueField(.mouseEventPressure, value: 1)
|
|
288
301
|
try postPointerEvent(
|
|
@@ -295,9 +308,9 @@ func targetedDrag(
|
|
|
295
308
|
gesturePhase: 3,
|
|
296
309
|
clickGroup: group
|
|
297
310
|
)
|
|
298
|
-
|
|
311
|
+
if progress >= 1 { break }
|
|
312
|
+
usleep(8_333)
|
|
299
313
|
}
|
|
300
|
-
usleep(50_000)
|
|
301
314
|
|
|
302
315
|
let up = try pointerEvent(source: source, type: .leftMouseUp, button: .left)
|
|
303
316
|
up.setDoubleValueField(.mouseEventPressure, value: 0)
|