Files
orca/src/cli/computer-tree-query.ts
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

60 lines
2.1 KiB
TypeScript

import {
joinComputerTreeText,
parseComputerTreeText,
type ComputerTreeElement
} from '../shared/computer-use-tree-text'
export type ComputerTreeQuery = {
query?: string
maxElements?: number
}
// Runs in the CLI over the rendered tree, so it works against every runtime version and provider.
export function queryComputerTreeText(treeText: string, options: ComputerTreeQuery): string {
const { header, elements, footer } = parseComputerTreeText(treeText)
const needle = options.query?.toLowerCase()
const candidates = needle
? elements.filter((element) => elementText(element).toLowerCase().includes(needle))
: elements
const shown =
options.maxElements === undefined ? candidates : candidates.slice(0, options.maxElements)
const shownSet = new Set(shown)
const keep = withAncestors(elements, shownSet)
const body = elements.flatMap((element) =>
!keep.has(element) ? [] : shownSet.has(element) ? element.lines : element.lines.slice(0, 1)
)
const note = needle
? candidates.length === 0
? `No elements match "${options.query}".`
: `${candidates.length} of ${elements.length} elements match "${options.query}"; showing ${shown.length === candidates.length ? 'them' : `the first ${shown.length}`} with their ancestors.`
: shown.length < elements.length
? `Showing the first ${shown.length} of ${elements.length} elements.`
: null
return joinComputerTreeText(header, note ? [note, ...body] : body, footer)
}
function elementText(element: ComputerTreeElement): string {
return element.lines[0].trimStart().replace(/^\d+ ?/u, '')
}
function withAncestors(
elements: ComputerTreeElement[],
shown: Set<ComputerTreeElement>
): Set<ComputerTreeElement> {
const keep = new Set<ComputerTreeElement>()
const path: (ComputerTreeElement | undefined)[] = []
for (const element of elements) {
path.length = Math.min(path.length, element.depth)
if (shown.has(element)) {
for (const ancestor of path) {
if (ancestor) {
keep.add(ancestor)
}
}
keep.add(element)
}
path[element.depth] = element
}
return keep
}