Files
orca/native/computer-use-macos/Sources/OrcaComputerUseMacOSCore/ActionArgumentValidation.swift
T
Brennan Benson 66dfdc456f feat(computer-use): support macOS middle click and stop the silent left-click fallback (#14721)
* feat(computer-use): support macOS middle click and gate the AX click path

`--mouse-button middle` already validated end-to-end through the CLI, the
zod schema, and the provider validator, and both the Windows and Linux
providers honored it. Only the macOS provider rejected it outright with
"middle-click is not yet supported", so the flag was a dead end on the one
platform that has no fallback.

Two changes:

- Add `.middle` to the macOS button mapping. macOS has no dedicated middle
  event family, so it rides `otherMouseDown`/`otherMouseUp` with the button
  number carried by `mouseButton: .center`; that constructor argument is
  honored for exactly the `otherMouse*` types, so no extra field write is
  needed.
- Validate the requested button before the accessibility fast path, and skip
  that path for buttons it cannot express. Previously the raw string was read
  unvalidated, and `performClickAction` only special-cased `right`, so
  `click --mouse-button middle --element-index N` (no modifiers, count 1) fell
  through to `AXPress` — a left click — and reported success with
  `path: "accessibility"`. Any unrecognized button string did the same. This
  matches guards the Windows and Linux providers already had.

The button enum moves into `OrcaComputerUseMacOSCore` so it is unit-testable;
`main.swift` keeps only the CoreGraphics mapping.

Also documents `--mouse-button` in the computer-use skill guide, which never
mentioned the flag, so agents on Windows and Linux had no way to discover it.

* test(computer-use): cover macOS middle click in the real-desktop e2e suite

* test(computer-use): prove macOS middle-click delivery
2026-08-15 00:41:45 -07:00

65 lines
2.2 KiB
Swift

public struct ActionArgumentValidationError: Error, Equatable {
public let message: String
public init(_ message: String) {
self.message = message
}
}
public enum MouseButtonSelection: String, Equatable, Sendable, CaseIterable {
case left
case right
case middle
/// Why: AXPress/AXShowMenu only model primary and secondary intent, so a middle
/// click has no accessibility equivalent and must reach the app as real events.
public var hasAccessibilityAction: Bool {
self != .middle
}
}
public enum ActionArgumentValidation {
public static func positiveInteger(
_ value: Double?,
defaultValue: Int,
name: String
) -> Result<Int, ActionArgumentValidationError> {
guard let value else { return .success(defaultValue) }
guard value.isFinite, value > 0, let parsed = boundedInteger(value, as: Int.self) else {
return .failure(ActionArgumentValidationError("\(name) must be a positive integer"))
}
return .success(parsed)
}
public static func positiveNumber(
_ value: Double?,
defaultValue: Double,
name: String
) -> Result<Double, ActionArgumentValidationError> {
guard let value else { return .success(defaultValue) }
guard value.isFinite, value > 0 else {
return .failure(ActionArgumentValidationError("\(name) must be a positive number"))
}
return .success(value)
}
public static func mouseButton(
_ value: String?
) -> Result<MouseButtonSelection, ActionArgumentValidationError> {
guard let value else { return .success(.left) }
guard let button = MouseButtonSelection(rawValue: value) else {
return .failure(ActionArgumentValidationError("unsupported mouse button '\(value)'"))
}
return .success(button)
}
public static func scrollDirection(_ value: String) -> Result<String, ActionArgumentValidationError> {
switch value {
case "up", "down", "left", "right":
return .success(value)
default:
return .failure(ActionArgumentValidationError("unsupported scroll direction: \(value)"))
}
}
}