@anionex/dsh-computer-use 0.2.1 → 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 +240 -17
- 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 +234 -17
- package/src/providers/unsupported.ts +11 -1
- package/src/service.ts +100 -37
|
@@ -6,7 +6,7 @@ import Darwin
|
|
|
6
6
|
import Foundation
|
|
7
7
|
import ScreenCaptureKit
|
|
8
8
|
|
|
9
|
-
private let helperVersion = "0.
|
|
9
|
+
private let helperVersion = "0.3.0"
|
|
10
10
|
|
|
11
11
|
private struct HelperError: Error {
|
|
12
12
|
let code: String
|
|
@@ -860,7 +860,10 @@ private func actionResult(
|
|
|
860
860
|
]
|
|
861
861
|
}
|
|
862
862
|
|
|
863
|
-
private func performAction(
|
|
863
|
+
private func performAction(
|
|
864
|
+
_ request: [String: Any],
|
|
865
|
+
dragStartBarrier: (() throws -> Void)? = nil
|
|
866
|
+
) throws -> [String: Any] {
|
|
864
867
|
let appIdentity = try dictionary(request["app"], "request.app")
|
|
865
868
|
let selector: [String: Any] = [
|
|
866
869
|
"bundleId": try string(appIdentity["bundleId"], "app.bundleId"),
|
|
@@ -1023,9 +1026,25 @@ private func performAction(_ request: [String: Any]) throws -> [String: Any] {
|
|
|
1023
1026
|
let coordinateSpace = action["coordinateSpace"] as? String ?? "window"
|
|
1024
1027
|
let from = try windowPoint(action, window: current.snapshot.windowJSON, xKey: "fromX", yKey: "fromY", coordinateSpace: coordinateSpace)
|
|
1025
1028
|
let to = try windowPoint(action, window: current.snapshot.windowJSON, xKey: "toX", yKey: "toY", coordinateSpace: coordinateSpace)
|
|
1029
|
+
let cursorSpeed = try double(interaction["cursorSpeedPxPerSecond"], "request.interaction.cursorSpeedPxPerSecond")
|
|
1030
|
+
let cursorAcceleration = try double(
|
|
1031
|
+
interaction["cursorAccelerationPxPerSecondSquared"],
|
|
1032
|
+
"request.interaction.cursorAccelerationPxPerSecondSquared"
|
|
1033
|
+
)
|
|
1034
|
+
guard cursorSpeed.isFinite, cursorSpeed >= 100, cursorSpeed <= 50_000,
|
|
1035
|
+
cursorAcceleration.isFinite, cursorAcceleration >= 100, cursorAcceleration <= 500_000 else {
|
|
1036
|
+
throw fail("COMPUTER_PROVIDER_FAILURE", "request.interaction cursor motion is outside the supported range")
|
|
1037
|
+
}
|
|
1026
1038
|
let target = try pointerTarget(app: app, window: current.snapshot.windowJSON, at: from)
|
|
1039
|
+
try dragStartBarrier?()
|
|
1027
1040
|
try pointerAction {
|
|
1028
|
-
try targetedDrag(
|
|
1041
|
+
try targetedDrag(
|
|
1042
|
+
from: from,
|
|
1043
|
+
to: to,
|
|
1044
|
+
target: target,
|
|
1045
|
+
speedPxPerSecond: cursorSpeed,
|
|
1046
|
+
accelerationPxPerSecondSquared: cursorAcceleration
|
|
1047
|
+
)
|
|
1029
1048
|
}
|
|
1030
1049
|
return actionResult(channel: "coordinates", activation: current.activation, pointerInput: true, pointerRouting: "target-process")
|
|
1031
1050
|
case "perform-action":
|
|
@@ -1060,7 +1079,10 @@ private func performAction(_ request: [String: Any]) throws -> [String: Any] {
|
|
|
1060
1079
|
}
|
|
1061
1080
|
}
|
|
1062
1081
|
|
|
1063
|
-
private func handle(
|
|
1082
|
+
private func handle(
|
|
1083
|
+
_ request: [String: Any],
|
|
1084
|
+
dragStartBarrier: (() throws -> Void)? = nil
|
|
1085
|
+
) async throws -> Any {
|
|
1064
1086
|
guard (request["protocolVersion"] as? NSNumber)?.intValue == 1 else {
|
|
1065
1087
|
throw fail("COMPUTER_PROVIDER_FAILURE", "unsupported helper protocol version")
|
|
1066
1088
|
}
|
|
@@ -1112,8 +1134,36 @@ private func handle(_ request: [String: Any]) async throws -> Any {
|
|
|
1112
1134
|
screenshot = try await captureWindow(snapshot, path: path, required: mode == "required")
|
|
1113
1135
|
}
|
|
1114
1136
|
return observationJSON(snapshot, screenshot: screenshot)
|
|
1137
|
+
case "activate-for-cursor":
|
|
1138
|
+
let appIdentity = try dictionary(request["app"], "app")
|
|
1139
|
+
let app = try resolveApp([
|
|
1140
|
+
"bundleId": try string(appIdentity["bundleId"], "app.bundleId"),
|
|
1141
|
+
"pid": try int(appIdentity["pid"], "app.pid"),
|
|
1142
|
+
])
|
|
1143
|
+
let options = try dictionary(request["options"], "options")
|
|
1144
|
+
let snapshot = try observeSnapshot(app: app, limits: options)
|
|
1145
|
+
let expected = try string(request["expectedStateHash"], "expectedStateHash")
|
|
1146
|
+
guard snapshot.stateHash == expected else {
|
|
1147
|
+
throw fail("COMPUTER_STALE_OBSERVATION", "the application UI changed before foreground activation")
|
|
1148
|
+
}
|
|
1149
|
+
if app.isActive {
|
|
1150
|
+
return ["observation": observationJSON(snapshot, screenshot: nil), "activation": "already-frontmost"]
|
|
1151
|
+
}
|
|
1152
|
+
let actionTimeoutMs = try int(request["actionTimeoutMs"], "actionTimeoutMs")
|
|
1153
|
+
guard actionTimeoutMs >= 1_000 && actionTimeoutMs <= 120_000 else {
|
|
1154
|
+
throw fail("COMPUTER_PROVIDER_FAILURE", "actionTimeoutMs must be between 1000 and 120000")
|
|
1155
|
+
}
|
|
1156
|
+
try activate(app, timeoutMs: actionTimeoutMs)
|
|
1157
|
+
let refreshed = try observeSnapshot(app: app, limits: options)
|
|
1158
|
+
guard activationStateMatches(snapshot, current: refreshed) else {
|
|
1159
|
+
throw fail("COMPUTER_STALE_OBSERVATION", "the application UI changed while the target application was activated")
|
|
1160
|
+
}
|
|
1161
|
+
return ["observation": observationJSON(refreshed, screenshot: nil), "activation": "activated"]
|
|
1115
1162
|
case "act":
|
|
1116
|
-
return try performAction(
|
|
1163
|
+
return try performAction(
|
|
1164
|
+
try dictionary(request["request"], "request"),
|
|
1165
|
+
dragStartBarrier: dragStartBarrier
|
|
1166
|
+
)
|
|
1117
1167
|
default:
|
|
1118
1168
|
throw fail("COMPUTER_PROVIDER_FAILURE", "unknown helper command")
|
|
1119
1169
|
}
|
|
@@ -1126,6 +1176,13 @@ private func emit(_ payload: [String: Any], exitCode: Int32) -> Never {
|
|
|
1126
1176
|
Darwin.exit(exitCode)
|
|
1127
1177
|
}
|
|
1128
1178
|
|
|
1179
|
+
private func emitFrame(_ payload: [String: Any]) {
|
|
1180
|
+
let data = (try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]))
|
|
1181
|
+
?? Data("{\"ok\":false,\"error\":{\"code\":\"COMPUTER_PROVIDER_FAILURE\",\"message\":\"response serialization failed\"}}".utf8)
|
|
1182
|
+
FileHandle.standardOutput.write(data)
|
|
1183
|
+
FileHandle.standardOutput.write(Data("\n".utf8))
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1129
1186
|
@main
|
|
1130
1187
|
private struct HelperMain {
|
|
1131
1188
|
@MainActor
|
|
@@ -1138,6 +1195,24 @@ private struct HelperMain {
|
|
|
1138
1195
|
CursorOverlayRuntime.run()
|
|
1139
1196
|
return
|
|
1140
1197
|
}
|
|
1198
|
+
if ProcessInfo.processInfo.arguments.contains("--drag-action") {
|
|
1199
|
+
guard let line = readLine(),
|
|
1200
|
+
let data = line.data(using: .utf8),
|
|
1201
|
+
let request = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
1202
|
+
(request["command"] as? String) == "act",
|
|
1203
|
+
let actionRequest = request["request"] as? [String: Any],
|
|
1204
|
+
let action = actionRequest["action"] as? [String: Any],
|
|
1205
|
+
(action["kind"] as? String) == "drag" else {
|
|
1206
|
+
throw fail("COMPUTER_PROVIDER_FAILURE", "drag helper stdin must contain one drag action request")
|
|
1207
|
+
}
|
|
1208
|
+
let value = try await handle(request, dragStartBarrier: {
|
|
1209
|
+
emitFrame(["ok": true, "event": "drag-ready"])
|
|
1210
|
+
guard readLine() == "START" else {
|
|
1211
|
+
throw fail("COMPUTER_CANCELLED", "drag action was cancelled before native mouse-down")
|
|
1212
|
+
}
|
|
1213
|
+
})
|
|
1214
|
+
emit(["ok": true, "value": value], exitCode: 0)
|
|
1215
|
+
}
|
|
1141
1216
|
guard let data = try FileHandle.standardInput.readToEnd(), !data.isEmpty,
|
|
1142
1217
|
let request = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
1143
1218
|
throw fail("COMPUTER_PROVIDER_FAILURE", "stdin must contain one JSON request")
|
|
@@ -54,6 +54,8 @@ private var maximumCursorDistance = 0.0
|
|
|
54
54
|
private var observedFrontmostPids = Set<Int32>()
|
|
55
55
|
private var maximumMatchingWindowCount = 0
|
|
56
56
|
private var matchingWindowFrames: [[String: Any]] = []
|
|
57
|
+
private var matchingWindowNumbers: [Int] = []
|
|
58
|
+
private var seenWindowFrames = Set<String>()
|
|
57
59
|
private var monitoredSourcePointerEvents = 0
|
|
58
60
|
private var pointerEventSourceCounts: [String: Int] = [:]
|
|
59
61
|
private var samples = 0
|
|
@@ -110,7 +112,16 @@ while DispatchTime.now().uptimeNanoseconds < deadline {
|
|
|
110
112
|
maximumMatchingWindowCount = max(maximumMatchingWindowCount, windows.count)
|
|
111
113
|
if let first = windows.first,
|
|
112
114
|
let bounds = first[kCGWindowBounds as String] as? [String: Any] {
|
|
113
|
-
|
|
115
|
+
let frameKey = ["X", "Y", "Width", "Height"]
|
|
116
|
+
.map { String(describing: bounds[$0] ?? "") }
|
|
117
|
+
.joined(separator: ",")
|
|
118
|
+
if seenWindowFrames.insert(frameKey).inserted {
|
|
119
|
+
matchingWindowFrames.append(bounds)
|
|
120
|
+
}
|
|
121
|
+
if let number = (first[kCGWindowNumber as String] as? NSNumber)?.intValue,
|
|
122
|
+
!matchingWindowNumbers.contains(number) {
|
|
123
|
+
matchingWindowNumbers.append(number)
|
|
124
|
+
}
|
|
114
125
|
}
|
|
115
126
|
samples += 1
|
|
116
127
|
if eventTapSource != nil {
|
|
@@ -121,15 +132,18 @@ while DispatchTime.now().uptimeNanoseconds < deadline {
|
|
|
121
132
|
}
|
|
122
133
|
|
|
123
134
|
let finalCursor = cursorLocation()
|
|
135
|
+
let finalFrontmostPid = frontmostPid()
|
|
124
136
|
let result: [String: Any] = [
|
|
125
137
|
"baselineCursor": ["x": baselineCursor.x, "y": baselineCursor.y],
|
|
126
138
|
"finalCursor": ["x": finalCursor.x, "y": finalCursor.y],
|
|
127
139
|
"maximumCursorDistance": maximumCursorDistance,
|
|
128
140
|
"baselineFrontmostPid": baselineFrontmostPid.map(Int.init) as Any,
|
|
129
141
|
"observedFrontmostPids": observedFrontmostPids.sorted().map(Int.init),
|
|
142
|
+
"finalFrontmostPid": finalFrontmostPid.map(Int.init) as Any,
|
|
130
143
|
"samples": samples,
|
|
131
144
|
"maximumMatchingWindowCount": maximumMatchingWindowCount,
|
|
132
145
|
"matchingWindowFrames": matchingWindowFrames,
|
|
146
|
+
"matchingWindowNumbers": matchingWindowNumbers,
|
|
133
147
|
"eventTapAvailable": eventTap != nil,
|
|
134
148
|
"monitoredSourcePointerEvents": monitoredSourcePointerEvents,
|
|
135
149
|
"pointerEventSourceCounts": pointerEventSourceCounts,
|
|
Binary file
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"helperVersion": "0.
|
|
4
|
-
"sourceSha256": "
|
|
3
|
+
"helperVersion": "0.3.0",
|
|
4
|
+
"sourceSha256": "f82877b40aefe9c5b49ca5aac743267e57cf7e87be623c975d535e4af03a788a",
|
|
5
5
|
"binary": {
|
|
6
6
|
"path": "bin/dsh-computer-use-helper",
|
|
7
|
-
"sha256": "
|
|
7
|
+
"sha256": "91fc66597ecb05a74123b5fde5d0c648963cbebad72ace2cfe6e5004852dd0cc",
|
|
8
8
|
"architectures": [
|
|
9
9
|
"arm64",
|
|
10
10
|
"x86_64"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anionex/dsh-computer-use",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Accessibility-first macOS Computer Use capability for DeepSeek Harness with stale-observation protection, app leases, confirmations, screenshots, and Web diagnostics",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek",
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
"url": "https://github.com/Anionex/dsh-computer-use/issues"
|
|
19
19
|
},
|
|
20
20
|
"type": "module",
|
|
21
|
-
"packageManager": "pnpm@11.20.0",
|
|
22
21
|
"engines": {
|
|
23
22
|
"node": "^22.19.0 || >=24.0.0"
|
|
24
23
|
},
|
|
@@ -58,20 +57,6 @@
|
|
|
58
57
|
"README.zh.md",
|
|
59
58
|
"LICENSE"
|
|
60
59
|
],
|
|
61
|
-
"scripts": {
|
|
62
|
-
"native:build": "node scripts/build-native.mjs",
|
|
63
|
-
"check:native": "node scripts/check-native.mjs",
|
|
64
|
-
"build": "pnpm run native:build && tsc -p tsconfig.json && tsc -p tsconfig.client.json && node scripts/build-client.mjs",
|
|
65
|
-
"prepack": "pnpm run build",
|
|
66
|
-
"test": "pnpm run build && vitest run tests",
|
|
67
|
-
"test:model": "node scripts/model-e2e.mjs",
|
|
68
|
-
"test:watch": "vitest tests",
|
|
69
|
-
"validate": "node scripts/validate.mjs --lane all",
|
|
70
|
-
"validate:local": "node scripts/validate.mjs --lane local",
|
|
71
|
-
"validate:profile": "node scripts/validate.mjs --lane profile",
|
|
72
|
-
"validate:model": "node scripts/model-e2e.mjs",
|
|
73
|
-
"validate:release": "pnpm run validate && pnpm run validate:model"
|
|
74
|
-
},
|
|
75
60
|
"dsh": {
|
|
76
61
|
"bundle": {
|
|
77
62
|
"patch": "./cordis.patch.yml"
|
|
@@ -124,5 +109,18 @@
|
|
|
124
109
|
"dependencies": {
|
|
125
110
|
"zod": "^4.4.3"
|
|
126
111
|
},
|
|
127
|
-
"license": "MIT"
|
|
128
|
-
|
|
112
|
+
"license": "MIT",
|
|
113
|
+
"scripts": {
|
|
114
|
+
"native:build": "node scripts/build-native.mjs",
|
|
115
|
+
"check:native": "node scripts/check-native.mjs",
|
|
116
|
+
"build": "pnpm run native:build && tsc -p tsconfig.json && tsc -p tsconfig.client.json && node scripts/build-client.mjs",
|
|
117
|
+
"test": "pnpm run build && vitest run tests",
|
|
118
|
+
"test:model": "node scripts/model-e2e.mjs",
|
|
119
|
+
"test:watch": "vitest tests",
|
|
120
|
+
"validate": "node scripts/validate.mjs --lane all",
|
|
121
|
+
"validate:local": "node scripts/validate.mjs --lane local",
|
|
122
|
+
"validate:profile": "node scripts/validate.mjs --lane profile",
|
|
123
|
+
"validate:model": "node scripts/model-e2e.mjs",
|
|
124
|
+
"validate:release": "pnpm run validate && pnpm run validate:model"
|
|
125
|
+
}
|
|
126
|
+
}
|
package/scripts/build-client.mjs
CHANGED
|
File without changes
|
package/scripts/build-native.mjs
CHANGED
|
File without changes
|
package/scripts/model-e2e.mjs
CHANGED
|
File without changes
|
package/scripts/validate.mjs
CHANGED
|
File without changes
|
package/src/backend.ts
CHANGED
|
@@ -74,6 +74,19 @@ export interface BackendActionResult {
|
|
|
74
74
|
export interface CursorVisibility {
|
|
75
75
|
readonly visible: boolean
|
|
76
76
|
readonly reason?: string
|
|
77
|
+
readonly reasonCode?: 'target-not-frontmost' | 'target-invalid'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Fresh state after a host-authorized foreground activation for cursor sequencing. */
|
|
81
|
+
export interface BackendCursorActivation {
|
|
82
|
+
readonly observation: BackendObservation
|
|
83
|
+
readonly activation: 'already-frontmost' | 'activated'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Native drag input and its Agent-cursor endpoint tracking result. */
|
|
87
|
+
export interface BackendTrackedDragResult {
|
|
88
|
+
readonly action: BackendActionResult
|
|
89
|
+
readonly cursor: CursorVisibility
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
export interface BackendCursorAction {
|
|
@@ -105,14 +118,16 @@ export interface ComputerUseBackend {
|
|
|
105
118
|
resolveApp(selector: ComputerAppSelector, signal: AbortSignal): Promise<ComputerAppIdentity>
|
|
106
119
|
listApps(signal: AbortSignal): Promise<ComputerAppSummary[]>
|
|
107
120
|
observe(app: ComputerAppIdentity, options: BackendObserveOptions, signal: AbortSignal): Promise<BackendObservation>
|
|
121
|
+
activateForCursor(app: ComputerAppIdentity, expectedStateHash: string, options: BackendObserveOptions, signal: AbortSignal): Promise<BackendCursorActivation>
|
|
108
122
|
act(request: BackendActionRequest, signal: AbortSignal): Promise<BackendActionResult>
|
|
123
|
+
actDragWithCursor(request: BackendActionRequest, cursor: BackendCursorAction & { kind: 'drag' }, signal: AbortSignal): Promise<BackendTrackedDragResult>
|
|
109
124
|
/**
|
|
110
125
|
* Drive the agent cursor for one action.
|
|
111
126
|
* @returns whether the cursor is on screen afterwards, plus why not when it
|
|
112
|
-
* is hidden.
|
|
113
|
-
*
|
|
127
|
+
* is hidden. The Service permits intentional background hiding but fails
|
|
128
|
+
* closed for foreground placement, transport, and target-validation failures.
|
|
114
129
|
*/
|
|
115
|
-
visualizeCursor(action: BackendCursorAction, phase: 'before' | 'after', signal: AbortSignal): Promise<CursorVisibility>
|
|
130
|
+
visualizeCursor(action: BackendCursorAction, phase: 'before' | 'during' | 'after', signal: AbortSignal): Promise<CursorVisibility>
|
|
116
131
|
dispose(): Promise<void>
|
|
117
132
|
health(signal: AbortSignal): Promise<BackendHealth>
|
|
118
133
|
openSettings(kind: 'accessibility' | 'screen-recording', signal: AbortSignal): Promise<void>
|
package/src/client/index.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
|
8
8
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
9
9
|
import type {} from '@deepseek-ai/dsh-settings/types'
|
|
10
10
|
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
11
|
+
import { integerInRange } from './settings-validation.js'
|
|
11
12
|
|
|
12
13
|
const NS = 'computer-use'
|
|
13
14
|
const ROUTE = '/_dsh/computer-use/settings'
|
|
@@ -22,7 +23,7 @@ const en = {
|
|
|
22
23
|
accessHint: 'Choose whether Computer Use may work with every app. Exact per-app rules remain available under Advanced settings.',
|
|
23
24
|
advanced: 'Advanced options',
|
|
24
25
|
advancedHint: 'Limits, helper path, cursor timing, and application grants.',
|
|
25
|
-
cursorTiming: '
|
|
26
|
+
cursorTiming: 'Agent cursor motion',
|
|
26
27
|
helper: 'Native helper',
|
|
27
28
|
helperUnknown: 'Unknown',
|
|
28
29
|
ready: 'Ready',
|
|
@@ -62,7 +63,9 @@ const en = {
|
|
|
62
63
|
cursorVisualization: 'Agent cursor',
|
|
63
64
|
cursorVisible: 'Show a separate click-through Agent cursor',
|
|
64
65
|
cursorHidden: 'Hide the Agent cursor',
|
|
65
|
-
|
|
66
|
+
cursorSpeed: 'Requested maximum cursor speed (px/s)',
|
|
67
|
+
cursorAcceleration: 'Cursor acceleration/deceleration (px/s²)',
|
|
68
|
+
cursorClickDelay: 'Delay after arrival before click (ms)',
|
|
66
69
|
cursorAutoHide: 'Cursor auto-hide (ms; 0 = stay visible)',
|
|
67
70
|
grants: 'Application grants',
|
|
68
71
|
grantsHint: 'One exact bundle id per line, followed by read or read,control. Wildcards are rejected.',
|
|
@@ -74,6 +77,7 @@ const en = {
|
|
|
74
77
|
readOnly: 'The current Settings provider is read-only.',
|
|
75
78
|
loading: 'Loading Computer Use settings...',
|
|
76
79
|
retry: 'Retry',
|
|
80
|
+
numberRange: '{field} must be an integer from {min} to {max}.',
|
|
77
81
|
} as const
|
|
78
82
|
|
|
79
83
|
type LocaleKey = keyof typeof en
|
|
@@ -87,7 +91,7 @@ const zh: Record<LocaleKey, string> = {
|
|
|
87
91
|
accessHint: '日常使用只需决定是否允许操作所有应用;指定应用规则可在高级设置中配置。',
|
|
88
92
|
advanced: '高级设置',
|
|
89
93
|
advancedHint: '操作方式、性能限制、光标效果、指定应用规则和运行组件。一般无需修改。',
|
|
90
|
-
cursorTiming: '
|
|
94
|
+
cursorTiming: '智能体光标移动',
|
|
91
95
|
helper: '运行组件版本',
|
|
92
96
|
helperUnknown: '未提供',
|
|
93
97
|
ready: '已就绪',
|
|
@@ -127,7 +131,9 @@ const zh: Record<LocaleKey, string> = {
|
|
|
127
131
|
cursorVisualization: '智能体光标',
|
|
128
132
|
cursorVisible: '显示单独的智能体光标',
|
|
129
133
|
cursorHidden: '隐藏智能体光标',
|
|
130
|
-
|
|
134
|
+
cursorSpeed: '光标期望最大速度(像素/秒)',
|
|
135
|
+
cursorAcceleration: '光标加/减速度(像素/秒²)',
|
|
136
|
+
cursorClickDelay: '到达后点击延迟(毫秒)',
|
|
131
137
|
cursorAutoHide: '光标自动隐藏(毫秒;0 = 保持显示)',
|
|
132
138
|
grants: '指定应用规则',
|
|
133
139
|
grantsHint: '每行填写一个应用标识,后接 read 或 read,control。应用标识必须完整,不能使用通配符。',
|
|
@@ -139,6 +145,7 @@ const zh: Record<LocaleKey, string> = {
|
|
|
139
145
|
readOnly: '当前配置为只读,无法在这里修改。',
|
|
140
146
|
loading: '正在加载电脑操作设置...',
|
|
141
147
|
retry: '重试',
|
|
148
|
+
numberRange: '{field}必须是 {min} 到 {max} 之间的整数。',
|
|
142
149
|
}
|
|
143
150
|
|
|
144
151
|
type Translate = (key: LocaleKey, params?: Record<string, string | number>) => string
|
|
@@ -161,6 +168,9 @@ interface ConfigValue {
|
|
|
161
168
|
pointerInputPolicy?: 'deny' | 'targeted'
|
|
162
169
|
cursorVisualization?: 'hidden' | 'visible'
|
|
163
170
|
cursorMotionMs?: number
|
|
171
|
+
cursorSpeedPxPerSecond?: number
|
|
172
|
+
cursorAccelerationPxPerSecondSquared?: number
|
|
173
|
+
cursorClickDelayMs?: number
|
|
164
174
|
cursorAutoHideMs?: number
|
|
165
175
|
}
|
|
166
176
|
allowAllApps?: boolean
|
|
@@ -284,7 +294,9 @@ interface Draft {
|
|
|
284
294
|
keyboardPolicy: 'preserve' | 'activate'
|
|
285
295
|
pointerInputPolicy: 'deny' | 'targeted'
|
|
286
296
|
cursorVisualization: 'hidden' | 'visible'
|
|
287
|
-
|
|
297
|
+
cursorSpeedPxPerSecond: string
|
|
298
|
+
cursorAccelerationPxPerSecondSquared: string
|
|
299
|
+
cursorClickDelayMs: string
|
|
288
300
|
cursorAutoHideMs: string
|
|
289
301
|
allowAllApps: boolean
|
|
290
302
|
grants: string
|
|
@@ -308,7 +320,9 @@ function draftOf(value: ConfigValue): Draft {
|
|
|
308
320
|
keyboardPolicy: value.interaction?.keyboardPolicy ?? 'preserve',
|
|
309
321
|
pointerInputPolicy: value.interaction?.pointerInputPolicy ?? 'targeted',
|
|
310
322
|
cursorVisualization: value.interaction?.cursorVisualization ?? 'visible',
|
|
311
|
-
|
|
323
|
+
cursorSpeedPxPerSecond: String(value.interaction?.cursorSpeedPxPerSecond ?? 1600),
|
|
324
|
+
cursorAccelerationPxPerSecondSquared: String(value.interaction?.cursorAccelerationPxPerSecondSquared ?? 6000),
|
|
325
|
+
cursorClickDelayMs: String(value.interaction?.cursorClickDelayMs ?? 90),
|
|
312
326
|
cursorAutoHideMs: String(value.interaction?.cursorAutoHideMs ?? 0),
|
|
313
327
|
allowAllApps: value.allowAllApps ?? false,
|
|
314
328
|
grants: (value.grants ?? []).map(grant => `${grant.bundleId} ${grant.control === true ? 'read,control' : 'read'}`).join('\n'),
|
|
@@ -321,7 +335,8 @@ function integer(value: string, name: string): number {
|
|
|
321
335
|
return parsed
|
|
322
336
|
}
|
|
323
337
|
|
|
324
|
-
function configOf(draft: Draft): ConfigValue {
|
|
338
|
+
function configOf(draft: Draft, t: Translate): ConfigValue {
|
|
339
|
+
const rangeError = (field: string, min: number, max: number): string => t('numberRange', { field, min, max })
|
|
325
340
|
const grants = draft.grants.split(/\r?\n/u).map(line => line.trim()).filter(Boolean).map((line) => {
|
|
326
341
|
const [bundleId, rawScopes, ...extra] = line.split(/\s+/u)
|
|
327
342
|
if (bundleId === undefined || rawScopes === undefined || extra.length > 0) throw new Error(`invalid grant line: ${line}`)
|
|
@@ -349,8 +364,10 @@ function configOf(draft: Draft): ConfigValue {
|
|
|
349
364
|
keyboardPolicy: draft.keyboardPolicy,
|
|
350
365
|
pointerInputPolicy: draft.pointerInputPolicy,
|
|
351
366
|
cursorVisualization: draft.cursorVisualization,
|
|
352
|
-
|
|
353
|
-
|
|
367
|
+
cursorSpeedPxPerSecond: integerInRange(draft.cursorSpeedPxPerSecond, t('cursorSpeed'), 100, 50000, rangeError),
|
|
368
|
+
cursorAccelerationPxPerSecondSquared: integerInRange(draft.cursorAccelerationPxPerSecondSquared, t('cursorAcceleration'), 100, 500000, rangeError),
|
|
369
|
+
cursorClickDelayMs: integerInRange(draft.cursorClickDelayMs, t('cursorClickDelay'), 0, 1000, rangeError),
|
|
370
|
+
cursorAutoHideMs: integerInRange(draft.cursorAutoHideMs, t('cursorAutoHide'), 0, 30000, rangeError),
|
|
354
371
|
},
|
|
355
372
|
allowAllApps: draft.allowAllApps,
|
|
356
373
|
grants,
|
|
@@ -396,12 +413,16 @@ function LoadedSettings({ controller, t }: { controller: ComputerUseSettingsCont
|
|
|
396
413
|
const save = (): void => {
|
|
397
414
|
try {
|
|
398
415
|
setDraftError(undefined)
|
|
399
|
-
void controller.action('save', { expectedRevision: snapshot.settings.revision, value: configOf(draft) }, 'save')
|
|
416
|
+
void controller.action('save', { expectedRevision: snapshot.settings.revision, value: configOf(draft, t) }, 'save')
|
|
400
417
|
} catch (error) {
|
|
401
418
|
setDraftError(error instanceof Error ? error.message : String(error))
|
|
402
419
|
}
|
|
403
420
|
}
|
|
404
|
-
const numberField = (key: keyof Draft, label: string
|
|
421
|
+
const numberField = (key: keyof Draft, label: string, bounds: { min?: number; max?: number } = {}) => (
|
|
422
|
+
<Field label={label}>
|
|
423
|
+
<Input type="number" min={bounds.min ?? 0} max={bounds.max} step={1} value={String(draft[key])} onChange={event => update(key, event.target.value as never)} />
|
|
424
|
+
</Field>
|
|
425
|
+
)
|
|
405
426
|
return <div className="dcu-settings">
|
|
406
427
|
{snapshot.provider.lastError === undefined ? null : <div className="dcu-alert error">{snapshot.provider.lastError}</div>}
|
|
407
428
|
{!snapshot.writable ? <div className="dcu-alert warning">{t('readOnly')}</div> : null}
|
|
@@ -469,8 +490,10 @@ function LoadedSettings({ controller, t }: { controller: ComputerUseSettingsCont
|
|
|
469
490
|
<section className="dcu-panel">
|
|
470
491
|
<div className="dcu-panel-title"><h3>{t('cursorTiming')}</h3></div>
|
|
471
492
|
<div className="dcu-grid">
|
|
472
|
-
{numberField('
|
|
473
|
-
{numberField('
|
|
493
|
+
{numberField('cursorSpeedPxPerSecond', t('cursorSpeed'), { min: 100, max: 50000 })}
|
|
494
|
+
{numberField('cursorAccelerationPxPerSecondSquared', t('cursorAcceleration'), { min: 100, max: 500000 })}
|
|
495
|
+
{numberField('cursorClickDelayMs', t('cursorClickDelay'), { max: 1000 })}
|
|
496
|
+
{numberField('cursorAutoHideMs', t('cursorAutoHide'), { max: 30000 })}
|
|
474
497
|
</div>
|
|
475
498
|
</section>
|
|
476
499
|
<section className="dcu-panel">
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Parse one bounded integer using caller-owned localized error copy. */
|
|
2
|
+
export function integerInRange(
|
|
3
|
+
value: string,
|
|
4
|
+
field: string,
|
|
5
|
+
min: number,
|
|
6
|
+
max: number,
|
|
7
|
+
formatError: (field: string, min: number, max: number) => string,
|
|
8
|
+
): number {
|
|
9
|
+
const parsed = Number(value)
|
|
10
|
+
if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) {
|
|
11
|
+
throw new Error(formatError(field, min, max))
|
|
12
|
+
}
|
|
13
|
+
return parsed
|
|
14
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -21,7 +21,11 @@ export interface ComputerUseInteractionConfig {
|
|
|
21
21
|
keyboardPolicy?: 'preserve' | 'activate'
|
|
22
22
|
pointerInputPolicy?: 'deny' | 'targeted'
|
|
23
23
|
cursorVisualization?: 'hidden' | 'visible'
|
|
24
|
+
/** @deprecated Accepted for 0.2.x settings compatibility; physical motion replaces fixed duration. */
|
|
24
25
|
cursorMotionMs?: number
|
|
26
|
+
cursorSpeedPxPerSecond?: number
|
|
27
|
+
cursorAccelerationPxPerSecondSquared?: number
|
|
28
|
+
cursorClickDelayMs?: number
|
|
25
29
|
cursorAutoHideMs?: number
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -67,7 +71,10 @@ export const Config: Schema<ComputerUseConfig> = z.object({
|
|
|
67
71
|
keyboardPolicy: z.union(['preserve', 'activate']).default('preserve'),
|
|
68
72
|
pointerInputPolicy: z.union(['deny', 'targeted']).default('targeted'),
|
|
69
73
|
cursorVisualization: z.union(['hidden', 'visible']).default('visible'),
|
|
70
|
-
cursorMotionMs: z.number()
|
|
74
|
+
cursorMotionMs: z.number(),
|
|
75
|
+
cursorSpeedPxPerSecond: z.number().default(1600),
|
|
76
|
+
cursorAccelerationPxPerSecondSquared: z.number().default(6000),
|
|
77
|
+
cursorClickDelayMs: z.number().default(90),
|
|
71
78
|
cursorAutoHideMs: z.number().default(0),
|
|
72
79
|
}),
|
|
73
80
|
allowAllApps: z.boolean().default(false),
|
|
@@ -99,7 +106,9 @@ export interface ResolvedComputerUseConfig {
|
|
|
99
106
|
keyboardPolicy: 'preserve' | 'activate'
|
|
100
107
|
pointerInputPolicy: 'deny' | 'targeted'
|
|
101
108
|
cursorVisualization: 'hidden' | 'visible'
|
|
102
|
-
|
|
109
|
+
cursorSpeedPxPerSecond: number
|
|
110
|
+
cursorAccelerationPxPerSecondSquared: number
|
|
111
|
+
cursorClickDelayMs: number
|
|
103
112
|
cursorAutoHideMs: number
|
|
104
113
|
}
|
|
105
114
|
allowAllApps: boolean
|
|
@@ -151,7 +160,12 @@ export function resolveConfig(config: ComputerUseConfig = {}): ResolvedComputerU
|
|
|
151
160
|
const keyboardPolicy = option('interaction.keyboardPolicy', config.interaction?.keyboardPolicy ?? 'preserve', ['preserve', 'activate'] as const)
|
|
152
161
|
const pointerInputPolicy = option('interaction.pointerInputPolicy', config.interaction?.pointerInputPolicy ?? 'targeted', ['deny', 'targeted'] as const)
|
|
153
162
|
const cursorVisualization = option('interaction.cursorVisualization', config.interaction?.cursorVisualization ?? 'visible', ['hidden', 'visible'] as const)
|
|
154
|
-
|
|
163
|
+
if (config.interaction?.cursorMotionMs !== undefined) {
|
|
164
|
+
integer('interaction.cursorMotionMs', config.interaction.cursorMotionMs, 0, 2000)
|
|
165
|
+
}
|
|
166
|
+
const cursorSpeedPxPerSecond = integer('interaction.cursorSpeedPxPerSecond', config.interaction?.cursorSpeedPxPerSecond ?? 1600, 100, 50000)
|
|
167
|
+
const cursorAccelerationPxPerSecondSquared = integer('interaction.cursorAccelerationPxPerSecondSquared', config.interaction?.cursorAccelerationPxPerSecondSquared ?? 6000, 100, 500000)
|
|
168
|
+
const cursorClickDelayMs = integer('interaction.cursorClickDelayMs', config.interaction?.cursorClickDelayMs ?? 90, 0, 1000)
|
|
155
169
|
const cursorAutoHideMs = integer('interaction.cursorAutoHideMs', config.interaction?.cursorAutoHideMs ?? 0, 0, 30000)
|
|
156
170
|
const allowAllApps = config.allowAllApps ?? false
|
|
157
171
|
const seen = new Set<string>()
|
|
@@ -187,7 +201,9 @@ export function resolveConfig(config: ComputerUseConfig = {}): ResolvedComputerU
|
|
|
187
201
|
keyboardPolicy,
|
|
188
202
|
pointerInputPolicy,
|
|
189
203
|
cursorVisualization,
|
|
190
|
-
|
|
204
|
+
cursorSpeedPxPerSecond,
|
|
205
|
+
cursorAccelerationPxPerSecondSquared,
|
|
206
|
+
cursorClickDelayMs,
|
|
191
207
|
cursorAutoHideMs,
|
|
192
208
|
},
|
|
193
209
|
allowAllApps,
|