@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.
Files changed (53) hide show
  1. package/README.md +13 -7
  2. package/README.zh.md +13 -7
  3. package/cordis.patch.yml +3 -0
  4. package/docs/interaction-policy.md +6 -4
  5. package/docs/interaction-policy.zh.md +7 -5
  6. package/lib/client.js +23 -11
  7. package/lib/client.js.map +1 -1
  8. package/lib/config.js +13 -3
  9. package/lib/config.js.map +1 -1
  10. package/lib/providers/macos.js +79 -14
  11. package/lib/providers/macos.js.map +1 -1
  12. package/lib/providers/native-helper.js +240 -17
  13. package/lib/providers/native-helper.js.map +1 -1
  14. package/lib/providers/unsupported.js +6 -0
  15. package/lib/providers/unsupported.js.map +1 -1
  16. package/lib/service.js +74 -32
  17. package/lib/service.js.map +1 -1
  18. package/lib/types/backend.d.ts +18 -3
  19. package/lib/types/backend.d.ts.map +1 -1
  20. package/lib/types/client/index.d.ts +5 -2
  21. package/lib/types/client/index.d.ts.map +1 -1
  22. package/lib/types/client/settings-validation.d.ts +3 -0
  23. package/lib/types/client/settings-validation.d.ts.map +1 -0
  24. package/lib/types/config.d.ts +7 -1
  25. package/lib/types/config.d.ts.map +1 -1
  26. package/lib/types/providers/macos.d.ts +8 -2
  27. package/lib/types/providers/macos.d.ts.map +1 -1
  28. package/lib/types/providers/native-helper.d.ts +9 -1
  29. package/lib/types/providers/native-helper.d.ts.map +1 -1
  30. package/lib/types/providers/unsupported.d.ts +6 -2
  31. package/lib/types/providers/unsupported.d.ts.map +1 -1
  32. package/lib/types/service.d.ts.map +1 -1
  33. package/native/macos/Sources/Fixture/main.swift +46 -0
  34. package/native/macos/Sources/Helper/CursorMotion.swift +67 -0
  35. package/native/macos/Sources/Helper/CursorOverlay.swift +334 -60
  36. package/native/macos/Sources/Helper/TargetedPointer.swift +23 -10
  37. package/native/macos/Sources/Helper/main.swift +80 -5
  38. package/native/macos/Sources/Monitor/main.swift +15 -1
  39. package/native/macos/bin/dsh-computer-use-helper +0 -0
  40. package/native/macos/manifest.json +3 -3
  41. package/package.json +16 -18
  42. package/scripts/build-client.mjs +0 -0
  43. package/scripts/build-native.mjs +0 -0
  44. package/scripts/model-e2e.mjs +0 -0
  45. package/scripts/validate.mjs +0 -0
  46. package/src/backend.ts +18 -3
  47. package/src/client/index.tsx +36 -13
  48. package/src/client/settings-validation.ts +14 -0
  49. package/src/config.ts +20 -4
  50. package/src/providers/macos.ts +102 -14
  51. package/src/providers/native-helper.ts +234 -17
  52. package/src/providers/unsupported.ts +11 -1
  53. 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.2.1"
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(_ request: [String: Any]) throws -> [String: Any] {
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(from: from, to: to, target: target)
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(_ request: [String: Any]) async throws -> Any {
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(try dictionary(request["request"], "request"))
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
- matchingWindowFrames = [bounds]
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,
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "helperVersion": "0.2.1",
4
- "sourceSha256": "c6939cc42fbe2f393390b3d15bf3a92ae4be4d1811d55427de3de8bed6628136",
3
+ "helperVersion": "0.3.0",
4
+ "sourceSha256": "f82877b40aefe9c5b49ca5aac743267e57cf7e87be623c975d535e4af03a788a",
5
5
  "binary": {
6
6
  "path": "bin/dsh-computer-use-helper",
7
- "sha256": "b29e2fb87995d70fe0a046e5d90998a23447c67a121c3e4de5b9ec9d9951435f",
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.2.1",
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
+ }
File without changes
File without changes
File without changes
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. A hidden cursor is not an error — native input is unaffected —
113
- * but the caller must be able to report that the user cannot see the agent.
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>
@@ -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: 'Cursor timing',
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
- cursorMotion: 'Cursor travel (ms)',
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
- cursorMotion: '光标移动时长(毫秒)',
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
- cursorMotionMs: string
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
- cursorMotionMs: String(value.interaction?.cursorMotionMs ?? 180),
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
- cursorMotionMs: integer(draft.cursorMotionMs, 'interaction.cursorMotionMs'),
353
- cursorAutoHideMs: integer(draft.cursorAutoHideMs, 'interaction.cursorAutoHideMs'),
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) => <Field label={label}><Input value={String(draft[key])} onChange={event => update(key, event.target.value as never)} /></Field>
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('cursorMotionMs', t('cursorMotion'))}
473
- {numberField('cursorAutoHideMs', t('cursorAutoHide'))}
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().default(180),
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
- cursorMotionMs: number
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
- const cursorMotionMs = integer('interaction.cursorMotionMs', config.interaction?.cursorMotionMs ?? 180, 0, 2000)
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
- cursorMotionMs,
204
+ cursorSpeedPxPerSecond,
205
+ cursorAccelerationPxPerSecondSquared,
206
+ cursorClickDelayMs,
191
207
  cursorAutoHideMs,
192
208
  },
193
209
  allowAllApps,