Skip to content

fix(kit): focus text fields and select outline rows on click - #153

Open
frankorbit wants to merge 2 commits into
iFurySt:mainfrom
frankorbit:pr/click-focus-row-select
Open

frankorbit wants to merge 2 commits into
iFurySt:mainfrom
frankorbit:pr/click-focus-row-select

Conversation

@frankorbit

@frankorbit frankorbit commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Measured with native AX on macOS 27.2 Contacts:

  • The SwiftUI search field exposes no AXPress, only AXConfirm / AXShowMenu. click falls through to AXConfirm, which submits the search while keyboard focus stays on the list, so the following type_text fails with “requires a focused editable text element”.
  • Sidebar group rows are AXRow elements whose parent is an AXOutline, not an AXList. selectContainingListItem does not cover them and the rows have no AXPress, so clicking a row never changed the selection.

Changes

  • click on AXTextField / AXTextArea first sets AXFocused = true and verifies that the app's focused element is now that element, before falling back to the AXPress / AXConfirm / AXOpen chain.
  • click on an element inside an AXRow sets AXSelected = true on the row and reads it back; it does not count as handled when the row is not settable.
  • press_key accepts ArrowUp / ArrowDown / ArrowLeft / ArrowRight.
  • The REPL's app.click({ x, y }) is equivalent to app.click([x, y]) (models use both spellings; the object form used to be rejected).

Verification

  • On device: clicking the Contacts search field followed by type_text("Yumi") filters the list; clicking the All on My Mac / All Contacts rows switches the selection.
  • Known unrelated failure: CursorWindowFollowTests.testServiceClickRegistersLiveWindowAndFeedbackMatchesMovedButton depends on the test host's own window and fails the same way on main here.

History: docs/histories/2026-11/20261106-1802-click-focus-text-fields-and-select-rows.md; release note added.

🤖 Generated with Claude Code

@frankorbit
frankorbit force-pushed the pr/click-focus-row-select branch from 177f2cd to 52ae199 Compare November 7, 2026 08:14
SwiftUI text fields such as the Contacts search box expose no AXPress, only
AXConfirm, so a click submitted the search and left keyboard focus on the
list; the following type_text then failed. Sidebar rows live in an
AXOutline, which selectContainingListItem does not cover, so clicking a row
never changed the selection.

- click on AXTextField/AXTextArea sets AXFocused and verifies the app's
  focused element before falling back to the action chain
- click on an element inside an AXRow sets AXSelected on the row and reads
  it back
- KeyPressParser accepts ArrowUp/ArrowDown/ArrowLeft/ArrowRight
- the REPL's app.click accepts a { x, y } point as well as [x, y]

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@frankorbit
frankorbit force-pushed the pr/click-focus-row-select branch from 52ae199 to 2f2511f Compare November 7, 2026 08:51
@hyprcat

hyprcat commented Nov 8, 2026

Copy link
Copy Markdown
Collaborator

The Contacts cases are real. We built the same kind of ladder and ran it against a compiled AppKit fixture on macOS 27.2. Three things came out of that which apply here:

  1. The new rungs fall through after writes that may have applied. setBoolAttribute and performAction turn .failure and .cannotComplete into false, so the chain moves on to AXPress, AXConfirm, descendants and then the pointer. AppKit answers -25205 for actions it did run. AXConfirm on a table cell returned -25205 and started editing the cell (the focus moved in within 38–43 ms). AXScrollDownByPage returned -25205 and scrolled a page. Notes returns -25205 while it creates the note, so a chain that moves on makes two notes.

    • For a write, only attributeUnsupported / actionUnsupported / illegalArgument / noValue / notImplemented prove nothing applied. For an action, only actionUnsupported / illegalArgument / notImplemented do. The AXOpen + attributeUnsupported special case already in performAction is the same lesson.
    • The same goes for a write that answered success but hasn't shown yet. If focusTextEntry returns false at 300 ms, the chain sends the very AXConfirm this PR is avoiding. If selectContainingRow returns false, AXPress/AXConfirm go into the row. Once a write went out, report it as delivered but unverified instead of trying another way.
    • The focus can also land on the field's own editor rather than the field itself (Preview's PDF form fields focus a text area inside). Accepting a descendant a few levels down avoids that false negative.
  2. Rows whose own AXSelected isn't settable need their container. Write [row] to the outline's or table's AXSelectedRows (or AXSelectedChildren, whichever is settable) and read back that the row is in it. Falling into the chain instead reaches the cell's text field, whose AXConfirm starts editing (see above) and selects nothing.

  3. A control inside a row now selects the row and never gets pressed. selectContainingRow climbs from any element and runs before AXPress. A click on a checkbox or a disclosure triangle in an outline row therefore selects the row and never toggles. Taking the row path only when the target is the row or cell itself, or has no AXPress of its own (a label), keeps those working.

Minor: the fixed 150 ms and 30 ms waits can be polls. On the fixture, writing AXSelected and reading it back took 0.7–2.2 ms end to end, polling every 5 ms with a 400 ms limit. With OPEN_COMPUTER_USE_ACTION_READ_BACK=0, pauseBeforeReadBack returns at once, so the row read-back can run before the selection shows.

…ce a write went out

selectContainingRow ran before AXPress for every element under an AXRow,
so a checkbox or disclosure triangle inside an outline row selected the
row and was never pressed. The row path now applies only when the target
is the row or cell itself or has no AXPress of its own.

AXFocused / AXSelected writes are treated as delivered unless the error
proves nothing applied (attributeUnsupported, actionUnsupported,
illegalArgument, noValue, notImplemented, invalidUIElement); AppKit
answers -25205 for writes it did run. When the read-back does not
confirm within 300 ms (focus) or 400 ms (row), the click stops instead
of falling through to AXConfirm, and the result carries a note asking
for get_app_state. Focus landing on the field's own editor counts as
confirmed. Rows whose AXSelected is not settable are selected through
the outline's or table's AXSelectedRows / AXSelectedChildren. Fixed
waits became 5 ms polls.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@frankorbit

Copy link
Copy Markdown
Contributor Author

Thanks, the three cases are real and 3a48b3e addresses them:

  1. AXFocused / AXSelected writes are now treated as delivered unless the error proves nothing applied (attributeUnsupported, actionUnsupported, illegalArgument, noValue, notImplemented, invalidUIElement); .failure / .cannotComplete no longer let the chain fall through to AXPress / AXConfirm. When the read-back does not confirm within 300 ms (focus) / 400 ms (row), the click stops there and the result carries a "delivered, not confirmed, run get_app_state" note instead of trying another way. Focus landing on the field's own editor (up to four levels down) counts as confirmed.
  2. Rows whose own AXSelected is not settable are selected by writing [row] to the outline's / table's AXSelectedRows or AXSelectedChildren, whichever is settable, with the membership read back.
  3. The row path now runs only when the target is the row or cell itself, or has no AXPress of its own, so checkboxes and disclosure triangles inside a row are pressed as before.

The fixed 150 ms / 30 ms waits are 5 ms polls now and no longer depend on OPEN_COMPUTER_USE_ACTION_READ_BACK. Not re-run against Contacts / Notes live yet.

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.

2 participants