mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 01:27:35 +00:00
* 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
60 lines
2.1 KiB
TypeScript
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
|
|
}
|