@anionex/dsh-computer-use 0.2.1 → 0.3.1

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 (50) 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 +31 -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 +7 -2
  21. package/lib/types/client/index.d.ts.map +1 -1
  22. package/lib/types/config.d.ts +7 -1
  23. package/lib/types/config.d.ts.map +1 -1
  24. package/lib/types/providers/macos.d.ts +8 -2
  25. package/lib/types/providers/macos.d.ts.map +1 -1
  26. package/lib/types/providers/native-helper.d.ts +9 -1
  27. package/lib/types/providers/native-helper.d.ts.map +1 -1
  28. package/lib/types/providers/unsupported.d.ts +6 -2
  29. package/lib/types/providers/unsupported.d.ts.map +1 -1
  30. package/lib/types/service.d.ts.map +1 -1
  31. package/native/macos/Sources/Fixture/main.swift +46 -0
  32. package/native/macos/Sources/Helper/CursorMotion.swift +67 -0
  33. package/native/macos/Sources/Helper/CursorOverlay.swift +334 -60
  34. package/native/macos/Sources/Helper/TargetedPointer.swift +23 -10
  35. package/native/macos/Sources/Helper/main.swift +80 -5
  36. package/native/macos/Sources/Monitor/main.swift +15 -1
  37. package/native/macos/bin/dsh-computer-use-helper +0 -0
  38. package/native/macos/manifest.json +3 -3
  39. package/package.json +16 -18
  40. package/scripts/build-client.mjs +0 -0
  41. package/scripts/build-native.mjs +0 -0
  42. package/scripts/model-e2e.mjs +0 -0
  43. package/scripts/validate.mjs +0 -0
  44. package/src/backend.ts +18 -3
  45. package/src/client/index.tsx +50 -13
  46. package/src/config.ts +20 -4
  47. package/src/providers/macos.ts +102 -14
  48. package/src/providers/native-helper.ts +234 -17
  49. package/src/providers/unsupported.ts +11 -1
  50. 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.1"
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.1",
4
+ "sourceSha256": "2239f57042d97cb55c7cf1ad2235ea3493457f761dbe16533d2bc9e302cbe42d",
5
5
  "binary": {
6
6
  "path": "bin/dsh-computer-use-helper",
7
- "sha256": "b29e2fb87995d70fe0a046e5d90998a23447c67a121c3e4de5b9ec9d9951435f",
7
+ "sha256": "545db079a227affc503c93ffed69cadf9306407de8f8bc8494d90f2948a3f600",
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.1",
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>
@@ -9,6 +9,21 @@ 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
11
 
12
+ /** Parse one bounded integer using caller-owned localized error copy. */
13
+ export function integerInRange(
14
+ value: string,
15
+ field: string,
16
+ min: number,
17
+ max: number,
18
+ formatError: (field: string, min: number, max: number) => string,
19
+ ): number {
20
+ const parsed = Number(value)
21
+ if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) {
22
+ throw new Error(formatError(field, min, max))
23
+ }
24
+ return parsed
25
+ }
26
+
12
27
  const NS = 'computer-use'
13
28
  const ROUTE = '/_dsh/computer-use/settings'
14
29
 
@@ -22,7 +37,7 @@ const en = {
22
37
  accessHint: 'Choose whether Computer Use may work with every app. Exact per-app rules remain available under Advanced settings.',
23
38
  advanced: 'Advanced options',
24
39
  advancedHint: 'Limits, helper path, cursor timing, and application grants.',
25
- cursorTiming: 'Cursor timing',
40
+ cursorTiming: 'Agent cursor motion',
26
41
  helper: 'Native helper',
27
42
  helperUnknown: 'Unknown',
28
43
  ready: 'Ready',
@@ -62,7 +77,9 @@ const en = {
62
77
  cursorVisualization: 'Agent cursor',
63
78
  cursorVisible: 'Show a separate click-through Agent cursor',
64
79
  cursorHidden: 'Hide the Agent cursor',
65
- cursorMotion: 'Cursor travel (ms)',
80
+ cursorSpeed: 'Requested maximum cursor speed (px/s)',
81
+ cursorAcceleration: 'Cursor acceleration/deceleration (px/s²)',
82
+ cursorClickDelay: 'Delay after arrival before click (ms)',
66
83
  cursorAutoHide: 'Cursor auto-hide (ms; 0 = stay visible)',
67
84
  grants: 'Application grants',
68
85
  grantsHint: 'One exact bundle id per line, followed by read or read,control. Wildcards are rejected.',
@@ -74,6 +91,7 @@ const en = {
74
91
  readOnly: 'The current Settings provider is read-only.',
75
92
  loading: 'Loading Computer Use settings...',
76
93
  retry: 'Retry',
94
+ numberRange: '{field} must be an integer from {min} to {max}.',
77
95
  } as const
78
96
 
79
97
  type LocaleKey = keyof typeof en
@@ -87,7 +105,7 @@ const zh: Record<LocaleKey, string> = {
87
105
  accessHint: '日常使用只需决定是否允许操作所有应用;指定应用规则可在高级设置中配置。',
88
106
  advanced: '高级设置',
89
107
  advancedHint: '操作方式、性能限制、光标效果、指定应用规则和运行组件。一般无需修改。',
90
- cursorTiming: '光标效果',
108
+ cursorTiming: '智能体光标移动',
91
109
  helper: '运行组件版本',
92
110
  helperUnknown: '未提供',
93
111
  ready: '已就绪',
@@ -127,7 +145,9 @@ const zh: Record<LocaleKey, string> = {
127
145
  cursorVisualization: '智能体光标',
128
146
  cursorVisible: '显示单独的智能体光标',
129
147
  cursorHidden: '隐藏智能体光标',
130
- cursorMotion: '光标移动时长(毫秒)',
148
+ cursorSpeed: '光标期望最大速度(像素/秒)',
149
+ cursorAcceleration: '光标加/减速度(像素/秒²)',
150
+ cursorClickDelay: '到达后点击延迟(毫秒)',
131
151
  cursorAutoHide: '光标自动隐藏(毫秒;0 = 保持显示)',
132
152
  grants: '指定应用规则',
133
153
  grantsHint: '每行填写一个应用标识,后接 read 或 read,control。应用标识必须完整,不能使用通配符。',
@@ -139,6 +159,7 @@ const zh: Record<LocaleKey, string> = {
139
159
  readOnly: '当前配置为只读,无法在这里修改。',
140
160
  loading: '正在加载电脑操作设置...',
141
161
  retry: '重试',
162
+ numberRange: '{field}必须是 {min} 到 {max} 之间的整数。',
142
163
  }
143
164
 
144
165
  type Translate = (key: LocaleKey, params?: Record<string, string | number>) => string
@@ -161,6 +182,9 @@ interface ConfigValue {
161
182
  pointerInputPolicy?: 'deny' | 'targeted'
162
183
  cursorVisualization?: 'hidden' | 'visible'
163
184
  cursorMotionMs?: number
185
+ cursorSpeedPxPerSecond?: number
186
+ cursorAccelerationPxPerSecondSquared?: number
187
+ cursorClickDelayMs?: number
164
188
  cursorAutoHideMs?: number
165
189
  }
166
190
  allowAllApps?: boolean
@@ -284,7 +308,9 @@ interface Draft {
284
308
  keyboardPolicy: 'preserve' | 'activate'
285
309
  pointerInputPolicy: 'deny' | 'targeted'
286
310
  cursorVisualization: 'hidden' | 'visible'
287
- cursorMotionMs: string
311
+ cursorSpeedPxPerSecond: string
312
+ cursorAccelerationPxPerSecondSquared: string
313
+ cursorClickDelayMs: string
288
314
  cursorAutoHideMs: string
289
315
  allowAllApps: boolean
290
316
  grants: string
@@ -308,7 +334,9 @@ function draftOf(value: ConfigValue): Draft {
308
334
  keyboardPolicy: value.interaction?.keyboardPolicy ?? 'preserve',
309
335
  pointerInputPolicy: value.interaction?.pointerInputPolicy ?? 'targeted',
310
336
  cursorVisualization: value.interaction?.cursorVisualization ?? 'visible',
311
- cursorMotionMs: String(value.interaction?.cursorMotionMs ?? 180),
337
+ cursorSpeedPxPerSecond: String(value.interaction?.cursorSpeedPxPerSecond ?? 1600),
338
+ cursorAccelerationPxPerSecondSquared: String(value.interaction?.cursorAccelerationPxPerSecondSquared ?? 6000),
339
+ cursorClickDelayMs: String(value.interaction?.cursorClickDelayMs ?? 90),
312
340
  cursorAutoHideMs: String(value.interaction?.cursorAutoHideMs ?? 0),
313
341
  allowAllApps: value.allowAllApps ?? false,
314
342
  grants: (value.grants ?? []).map(grant => `${grant.bundleId} ${grant.control === true ? 'read,control' : 'read'}`).join('\n'),
@@ -321,7 +349,8 @@ function integer(value: string, name: string): number {
321
349
  return parsed
322
350
  }
323
351
 
324
- function configOf(draft: Draft): ConfigValue {
352
+ function configOf(draft: Draft, t: Translate): ConfigValue {
353
+ const rangeError = (field: string, min: number, max: number): string => t('numberRange', { field, min, max })
325
354
  const grants = draft.grants.split(/\r?\n/u).map(line => line.trim()).filter(Boolean).map((line) => {
326
355
  const [bundleId, rawScopes, ...extra] = line.split(/\s+/u)
327
356
  if (bundleId === undefined || rawScopes === undefined || extra.length > 0) throw new Error(`invalid grant line: ${line}`)
@@ -349,8 +378,10 @@ function configOf(draft: Draft): ConfigValue {
349
378
  keyboardPolicy: draft.keyboardPolicy,
350
379
  pointerInputPolicy: draft.pointerInputPolicy,
351
380
  cursorVisualization: draft.cursorVisualization,
352
- cursorMotionMs: integer(draft.cursorMotionMs, 'interaction.cursorMotionMs'),
353
- cursorAutoHideMs: integer(draft.cursorAutoHideMs, 'interaction.cursorAutoHideMs'),
381
+ cursorSpeedPxPerSecond: integerInRange(draft.cursorSpeedPxPerSecond, t('cursorSpeed'), 100, 50000, rangeError),
382
+ cursorAccelerationPxPerSecondSquared: integerInRange(draft.cursorAccelerationPxPerSecondSquared, t('cursorAcceleration'), 100, 500000, rangeError),
383
+ cursorClickDelayMs: integerInRange(draft.cursorClickDelayMs, t('cursorClickDelay'), 0, 1000, rangeError),
384
+ cursorAutoHideMs: integerInRange(draft.cursorAutoHideMs, t('cursorAutoHide'), 0, 30000, rangeError),
354
385
  },
355
386
  allowAllApps: draft.allowAllApps,
356
387
  grants,
@@ -396,12 +427,16 @@ function LoadedSettings({ controller, t }: { controller: ComputerUseSettingsCont
396
427
  const save = (): void => {
397
428
  try {
398
429
  setDraftError(undefined)
399
- void controller.action('save', { expectedRevision: snapshot.settings.revision, value: configOf(draft) }, 'save')
430
+ void controller.action('save', { expectedRevision: snapshot.settings.revision, value: configOf(draft, t) }, 'save')
400
431
  } catch (error) {
401
432
  setDraftError(error instanceof Error ? error.message : String(error))
402
433
  }
403
434
  }
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>
435
+ const numberField = (key: keyof Draft, label: string, bounds: { min?: number; max?: number } = {}) => (
436
+ <Field label={label}>
437
+ <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)} />
438
+ </Field>
439
+ )
405
440
  return <div className="dcu-settings">
406
441
  {snapshot.provider.lastError === undefined ? null : <div className="dcu-alert error">{snapshot.provider.lastError}</div>}
407
442
  {!snapshot.writable ? <div className="dcu-alert warning">{t('readOnly')}</div> : null}
@@ -469,8 +504,10 @@ function LoadedSettings({ controller, t }: { controller: ComputerUseSettingsCont
469
504
  <section className="dcu-panel">
470
505
  <div className="dcu-panel-title"><h3>{t('cursorTiming')}</h3></div>
471
506
  <div className="dcu-grid">
472
- {numberField('cursorMotionMs', t('cursorMotion'))}
473
- {numberField('cursorAutoHideMs', t('cursorAutoHide'))}
507
+ {numberField('cursorSpeedPxPerSecond', t('cursorSpeed'), { min: 100, max: 50000 })}
508
+ {numberField('cursorAccelerationPxPerSecondSquared', t('cursorAcceleration'), { min: 100, max: 500000 })}
509
+ {numberField('cursorClickDelayMs', t('cursorClickDelay'), { max: 1000 })}
510
+ {numberField('cursorAutoHideMs', t('cursorAutoHide'), { max: 30000 })}
474
511
  </div>
475
512
  </section>
476
513
  <section className="dcu-panel">
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,