Files
orca/native/computer-use-macos/Sources/OrcaComputerUseMacOSCore/StableElementIndexes.swift
T
Kelvin Amoaba 030c88deec feat(computer): return tree diffs after actions and add get-app-state --query (#36023)
* feat(computer): send tree diffs after actions and add get-app-state --query

Action results now carry only the accessibility-tree lines that changed since
the caller's last view of the same window (~ changed, + added, - removed as
index ranges, or an explicit no-change line), plus the focused element. The
runtime sidecar keeps the last tree per session/worktree and app, so all three
providers get it. The CLI opts in with treeDiff; older runtimes strip the
param and keep returning the full tree, and --full-tree opts out.

The macOS helper now keeps an element's index across snapshots of a window:
AX identity first, then the next unclaimed index with the same signature for
views that rebuild their AX objects, and never reuses a removed index.

get-app-state --query prints matching elements with their ancestors and
--max-elements caps the output, both in the CLI over the rendered tree.

Custom action names with raw newlines no longer split an element over lines.

* fix(computer): preserve parent changes in snapshot diffs

* fix(computer): refuse stale index reuse for lookalike controls

* fix(computer): print full trees in action replies

* fix(computer): keep controls located in snapshot replies
2026-10-29 23:05:41 +00:00

41 lines
1.8 KiB
Swift

import Foundation
/// Keeps an element's index across snapshots of the same window, so action results can send only
/// the lines that changed.
public enum StableElementIndexes {
/// Only matching AX identities keep their indexes; all other elements receive fresh indexes.
public static func assign<Key: Hashable>(
current: [Key],
previous: [(key: Key, index: Int)],
previousNextFreshIndex: Int
) -> (indexes: [Int], nextFreshIndex: Int) {
let previousIndexByKey = Dictionary(previous.map { ($0.key, $0.index) }, uniquingKeysWith: { first, _ in first })
var claimed = Set<Int>()
var assigned = current.map { element -> Int? in
guard let index = previousIndexByKey[element], claimed.insert(index).inserted else { return nil }
return index
}
var nextFreshIndex = max(previousNextFreshIndex, (previous.map(\.index).max() ?? -1) + 1)
for position in assigned.indices where assigned[position] == nil {
assigned[position] = nextFreshIndex
nextFreshIndex += 1
}
return (assigned.compactMap { $0 }, nextFreshIndex)
}
/// Rewrites the leading `<tabs><index>` of a rendered element line; other lines pass through.
public static func renumber(line: String, indexes: [Int: Int]) -> String {
let tabs = line.prefix { $0 == "\t" }
let rest = line.dropFirst(tabs.count)
let digits = rest.prefix { $0.isASCII && $0.isNumber }
guard !digits.isEmpty,
rest.dropFirst(digits.count).first.map({ $0 == " " }) ?? true,
let old = Int(digits),
let new = indexes[old]
else {
return line
}
return "\(tabs)\(new)\(rest.dropFirst(digits.count))"
}
}