Skip to content

feat: axTree() — box model of a live application - #12

Merged
woladi merged 1 commit into
mainfrom
feat/ax-box-model
Aug 24, 2026
Merged

woladi merged 1 commit into
mainfrom
feat/ax-box-model

Conversation

@woladi

@woladi woladi commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Implements docs/BOX-MODEL.md phases 1–3: the accessibility tree of a running app as a box model, with optional colours and typography.

const tree = await axTree({ app: 'Safari' });
// nodes: [{ id, parent, role: 'Button', label: 'Zapisz', box: [812,540,96,32], style?, text? }]

Ships as a fourth prebuilt helper (ax-helper) through the existing native pipeline — nothing compiles on the user's machine.

Cost is bounded on purpose

Every attribute read is a synchronous IPC round trip, and the target app's implementation dominates rather than tree size — the same 4000 elements measured 1.6 s in Safari and 11 s in Finder. So: attribute reads are batched, offscreen subtrees culled, maxElements/maxDepth cap the walk, and budget reports what happened including capped: true. A truncated tree is never presented as complete.

Two things that only appeared once it ran

The obvious JSON cost more than the screenshot it replaces — ~12.8k tokens for 250 Safari nodes against ~6.9k for the image. Encoding box as [x,y,w,h] and omitting enabled: true / focused: false cut 44%; pruning unlabelled containers (detail: 'content', the default) cut another 48%, 600 → 289 nodes on Finder. Net ~25 tokens/node instead of ~51.

Even so, a full tree is still not a token saving over a screenshot (~7.3k vs ~6.9k on Finder). The README says that plainly rather than implying a saving that does not exist — the case for this is exact geometry, roles and enabled state, plus being able to take a slice.

Swift omits nil rather than encoding null, so the root node has no parent key. The TypeScript type promised number | null; a consumer checking === null would have been wrong. A test caught it; the type is now parent?: number.

Drive-by fix

captureScreen() accepted an invalid region: given a negative or fully offscreen rect, screencapture clamps and exits 0 on an unlocked Mac while failing elsewhere, so a caller's mistake surfaced as a corrupt 3.6 KB image. The rect is now validated against real display bounds. This also makes a pre-existing test deterministic — it asserted on screencapture's own behaviour and failed on any unlocked machine (verified: it fails on main too).

Verification

91/91 tests, including 8 new ones over axTree. Typecheck, lint and build clean. Exercised live against Finder, Safari, TextEdit and an Electron app.

🤖 Generated with Claude Code

Returns element boxes, hierarchy, roles and labels for a running app from
the accessibility API, optionally with colours sampled from a capture and
typography from the AX attributed string. Geometry is measured, not
inferred from OCR bounding boxes.

Ships as a fourth prebuilt helper through the existing native pipeline,
so nothing compiles on the user's machine.

Cost is bounded on purpose. Every attribute read is a synchronous IPC
round trip, and the target app's implementation dominates rather than
tree size — the same 4000 elements measured 1.6s in Safari and 11s in
Finder. Reads are batched, offscreen subtrees culled, the walk capped,
and `budget` reports what happened including `capped: true`, so a
truncated tree is never presented as complete.

Two things only showed up once it ran:

- The obvious JSON cost more than the screenshot it replaces — ~12.8k
  tokens for 250 Safari nodes against ~6.9k for the image. Encoding box
  as [x,y,w,h] and omitting default-valued fields cut 44%; pruning
  unlabelled containers cut another 48%. A full tree is still not a token
  saving over a screenshot, and the README says so instead of implying
  one — the case for this is exact geometry and semantics, plus being
  able to take a slice.
- Swift omits nil rather than encoding null, so the root has no `parent`
  key; the TypeScript type promised `number | null` and a consumer
  checking `=== null` would have been wrong. A test caught it.

Also fixes captureScreen() accepting an invalid region: given a negative
or fully offscreen rect, screencapture clamps and exits 0 on an unlocked
Mac while failing elsewhere, so the caller's mistake surfaced as corrupt
output. The rect is now checked against real display bounds. This also
makes a pre-existing test deterministic — it asserted on screencapture's
own behaviour and failed on any unlocked machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@woladi
woladi merged commit 6d11ec0 into main Aug 24, 2026
1 check passed
@woladi
woladi deleted the feat/ax-box-model branch August 24, 2026 16:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant