Skip to content

feat: element actions and value setting for non-text controls - #47

Merged
iamnbutler merged 1 commit into
mainfrom
feat/element-actions
Oct 17, 2026
Merged

iamnbutler merged 1 commit into
mainfrom
feat/element-actions

Conversation

@iamnbutler

@iamnbutler iamnbutler commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Adds two snapshot-bound tools for controls that clicking and typing handle badly.

Tools

  • desktop_element_action performs one named Accessibility action on an observed element: AXShowMenu, AXIncrement, AXDecrement, AXConfirm, AXCancel, AXPick, AXRaise or AXScrollToVisible. The client refuses before dispatch when the element doesn't list that action in attributes.actions. AXPress is left out because it duplicates desktop_click.
  • desktop_set_value sets a non-text control and checks the result by reading the same element back.
    • Numbers work for sliders, steppers (AXIncrementor) and level indicators.
    • true/false works for checkboxes, switches and disclosure triangles, where true expands.
    • When a toggle's value is read-only but the toggle advertises AXPress, as SwiftUI checkboxes do, it gets at most one press. If the state already matches, nothing is sent. A mixed state is refused.
    • Text roles are refused and pointed to desktop_type.
    • Pop-up buttons are documented as: desktop_click to open, then AXPick (or a click) on the item from the next inspection. Open menus are now listed first in ui_elements, so the 32 KB result bound doesn't trim them away.
  • desktop_inspect now adds attributes.actions to each element. It lists only the standard AX names, at most 16, from the supported set (AXPress is omitted).

Exactness (peekaboo-element-action.patch, last in the stack after peekaboo-layout-keys.patch from #46)

  • Upstream, performAction and setValue resolved the target by snapshot ID and fell back to a fuzzy label query. They then found the live element by a score in which a label or frame alone could win. The only check afterwards compared that live element with itself.
  • With the patch, both take only a literal element ID. The live element must match the observed receipt (process, window, role, frame, and identifier or title) before input and again right before the action. A mismatch is a pre-dispatch SNAPSHOT_STALE refusal.
  • This also makes desktop_type stricter, since it routes through setValue.
  • Named actions run off MainActor through DetachedAXActionRunner.
  • Upstream test fixtures that stand in a fake AX element inject a matcher that accepts it. Two new tests cover a mismatched live element and a label used in place of an ID.
  • The patch is documented in native/patches/README.md.

Verified

  • Rebased onto main at 3f95dd5 (feat: send every printable key and the remaining named keys #46) with this patch last.
    • npm run build:js && npm run check passes.
    • node dist/build.js --out build --scratch native/.build passes with the full patch stack.
  • Upstream PeekabooAutomationKit tests on the rebased stack: 168/168 passed for the filters ActionInputDriverTests|UIAutomationActionOutcomeProvidingTests|DetachedAXObservationWorker|AgentCursor|SetValue.
    • This includes the 2 new tests and the 4 fixture updates.
    • The run was isolated from the machine-wide ~/.peekaboo lane lock by a throwaway local edit that pointed DesktopCoordinationRuntimeRoot.defaultURL at a temp directory. The edit was reverted before pushing and is not part of this PR.
    • Setting HOME doesn't isolate the lock, because the lane root comes from the account database (getpwuid_r).
  • Tested live with a signed dev build (isolated state dir), System Settings > Sound. Everything was restored: volume 31, unmuted, alert sound Boop.
    • Output volume slider: desktop_set_value 0.25 completed with readback 0.25. AXIncrement went 0.25 to 0.35 and AXDecrement went back.
    • Mute checkbox (SwiftUI, read-only value): false when already unmuted was confirmed_no_change and sent no input. true was confirmed by a press with readback; then restored.
    • Alert sound pop-up: desktop_click opened the menu, the items listed AXPick, and AXPick on the current item completed.
    • Acting on a menu item after its menu closed was correctly refused as stale. The fuzzy resolver had picked the pop-up button instead.

Pending: screen locked

The Mac's screen locked partway through, so these live checks have not run:

  • AXShowMenu on a Finder file: check the context menu appears in the next inspection, then dismiss it with AXCancel.
  • Finder list view disclosure triangle: desktop_set_value true/false to expand and collapse, then restore.
  • TextEdit Format > Font panel stepper (AXIncrementor): AXIncrement/AXDecrement and desktop_set_value.
  • Displays brightness slider: set and restore.
  • Pop-up item change: pick Breeze, verify, then restore Boop.
  • Regression: desktop_type into a TextEdit or Settings search field under the exact binding.
  • AXConfirm, AXRaise and AXScrollToVisible on elements that advertise them.

Caveats

  • Date pickers aren't supported, because UIElementValue has no date type.
  • desktop_set_value compares numbers exactly on readback, so a control that rounds or snaps reports unknown. Re-inspect it rather than repeat.
  • A press-toggled checkbox reports accessibility_action delivery, not accessibility_value.
  • attributes.actions makes inspect results a few percent larger, still within the 32 KB bound.

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