Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@ Or add it to your own client manually:
}
```

### macOS image capture

`get_app_state` and action tools return a PNG screenshot together with the accessibility tree. On macOS, screenshot size can be tuned with optional environment variables. Set them before starting the `open-computer-use` runtime process; changing shell environment values afterward does not affect an already running process.

| Variable | Default | Meaning |
| --- | --- | --- |
| `OPEN_COMPUTER_USE_IMAGE_CAPTURE_TIMEOUT` | `5` | Seconds to wait for ScreenCaptureKit before omitting the screenshot image content from the result. |
| `OPEN_COMPUTER_USE_IMAGE_MAX_DIMENSION` | `1280` | Integer long-edge pixel cap for the returned PNG. |
| `OPEN_COMPUTER_USE_IMAGE_MAX_BYTES` | `900000` | Best-effort byte budget for the encoded PNG. |
| `OPEN_COMPUTER_USE_IMAGE_MIN_SCALE` | `0.25` | Byte-budget retry multiplier applied after the dimension cap. For example, a 500 px capped long edge with `0.25` may retry down to 125 px; it never enlarges the PNG chosen by the dimension cap. |

Invalid, non-finite, or non-positive values fall back to the defaults. `OPEN_COMPUTER_USE_IMAGE_MIN_SCALE` values above `1` also fall back.

Coordinate tools keep using the actual returned PNG dimensions, so downsampling does not change click or drag mapping.

### Skill

Install the skill directly:
Expand Down
15 changes: 15 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ ocu install-codex-mcp
}
```

### macOS 截图尺寸

`get_app_state` 和 action 类工具会和 accessibility tree 一起返回 PNG 截图。macOS 上可以通过可选环境变量调整截图尺寸。请在启动 `open-computer-use` runtime 进程前设置这些变量;之后再修改 shell 环境不会影响已经运行的进程。

| 变量 | 默认值 | 作用 |
| --- | --- | --- |
| `OPEN_COMPUTER_USE_IMAGE_CAPTURE_TIMEOUT` | `5` | ScreenCaptureKit 等待秒数;超时会从结果里省略截图 image 内容。 |
| `OPEN_COMPUTER_USE_IMAGE_MAX_DIMENSION` | `1280` | 返回 PNG 的整数长边像素上限。 |
| `OPEN_COMPUTER_USE_IMAGE_MAX_BYTES` | `900000` | 编码后 PNG 的 best-effort 字节预算。 |
| `OPEN_COMPUTER_USE_IMAGE_MIN_SCALE` | `0.25` | 作为长边上限之后的字节预算重试倍率。例如长边已限制到 500 px,`0.25` 允许继续重试缩小到 125 px;它不会放大长边上限已经选出的 PNG。 |

非法值、非有限数或非正数会回退到默认值。`OPEN_COMPUTER_USE_IMAGE_MIN_SCALE` 大于 `1` 时也会回退。

坐标类工具会读取实际返回 PNG 的尺寸做映射,因此降采样不会改变 click 或 drag 的坐标换算。

### Skill

一键安装skill:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ final class FixtureAppDelegate: NSObject, NSApplicationDelegate, NSTextFieldDele
private let dragPadView = DragPadView(frame: NSRect(x: 0, y: 0, width: 320, height: 120))
private var scrollView: NSScrollView!
private var counter = 0
private var selectedText: String?
private weak var observedScrollView: NSScrollView?
private var commandObserver: NSObjectProtocol?
private let headless: Bool
Expand Down Expand Up @@ -286,15 +287,18 @@ final class FixtureAppDelegate: NSObject, NSApplicationDelegate, NSTextFieldDele
switch (command.kind, command.identifier) {
case ("set_value", "fixture-input"):
inputField.stringValue = command.value ?? ""
selectedText = nil
updateExportedState()
case ("click", "fixture-increment"):
handleIncrement()
case ("click", "fixture-input"):
window.makeFirstResponder(inputField)
selectedText = nil
updateExportedState()
case ("click", "fixture-key-capture"):
window.makeFirstResponder(keyCaptureView)
keyCaptureView.needsDisplay = true
selectedText = nil
updateExportedState()
case ("scroll", "fixture-scroll-view"):
let delta = CGFloat(120 * (command.pages ?? 1))
Expand All @@ -321,17 +325,83 @@ final class FixtureAppDelegate: NSObject, NSApplicationDelegate, NSTextFieldDele
case ("type_text", "fixture-input"):
inputField.stringValue += command.value ?? ""
window.makeFirstResponder(inputField)
selectedText = nil
updateExportedState()
case ("select_text", "fixture-input"):
selectTextInFixtureInput(command)
updateExportedState()
case ("press_key", "fixture-key-capture"):
keyLabel.stringValue = "Last key: \(command.value ?? "unknown")"
window.makeFirstResponder(keyCaptureView)
keyCaptureView.needsDisplay = true
selectedText = nil
updateExportedState()
default:
break
}
}

private func selectTextInFixtureInput(_ command: FixtureCommand) {
let value = inputField.stringValue
guard let target = command.value,
let range = fixtureTextSelectionMatch(in: value, text: target, prefix: command.prefix, suffix: command.suffix)
else {
selectedText = nil
return
}

let mode = command.selection ?? "text"
let selectedRange = switch mode {
case "cursor_before":
NSRange(location: range.location, length: 0)
case "cursor_after":
NSRange(location: range.location + range.length, length: 0)
default:
range
}

window.makeFirstResponder(inputField)
if let editor = inputField.currentEditor() {
editor.selectedRange = selectedRange
}
selectedText = mode == "text" ? (value as NSString).substring(with: range) : nil
}

private func fixtureTextSelectionMatch(in value: String, text: String, prefix: String?, suffix: String?) -> NSRange? {
let haystack = value as NSString
let target = text as NSString
guard target.length > 0 else {
return nil
}

let prefix = prefix ?? ""
let suffix = suffix ?? ""
var searchRange = NSRange(location: 0, length: haystack.length)
var matchedRange: NSRange?

while searchRange.length >= target.length {
let found = haystack.range(of: text, options: [], range: searchRange)
if found.location == NSNotFound {
break
}

let afterStart = found.location + found.length
let before = haystack.substring(with: NSRange(location: 0, length: found.location))
let after = haystack.substring(with: NSRange(location: afterStart, length: haystack.length - afterStart))
if (prefix.isEmpty || before.hasSuffix(prefix)) && (suffix.isEmpty || after.hasPrefix(suffix)) {
if matchedRange != nil {
return nil
}
matchedRange = found
}

let nextLocation = found.location + max(found.length, 1)
searchRange = NSRange(location: nextLocation, length: haystack.length - nextLocation)
}

return matchedRange
}

private func updateExportedState() {
guard let contentView = window.contentView else {
return
Expand All @@ -341,6 +411,7 @@ final class FixtureAppDelegate: NSObject, NSApplicationDelegate, NSTextFieldDele
windowTitle: window.title,
windowBounds: FixtureRect(rect: windowBoundsInQuartzCoordinates()),
focusedIdentifier: focusedIdentifier(),
selectedText: selectedText,
elements: [
element(identifier: "fixture-window", index: 0, role: "standard window", title: window.title, value: nil, actions: ["Raise"], rect: CGRect(x: 0, y: 0, width: window.frame.width, height: window.frame.height)),
element(identifier: "fixture-increment", index: 1, role: "button", title: incrementButton.title, value: nil, actions: [], rect: localRect(for: incrementButton, in: contentView)),
Expand Down
Loading