Suppress AX capability flicker on the same focused element#494
Merged
Conversation
Apple Calendar's event-editor field intermittently drops one of the required AX attributes (textValue / selectionRange / caretBounds) during its own redraws, collapsing FocusCapabilityResolver to Blocked for a single ~13 ms poll before bouncing back to Supported on the next. FocusCapabilityResolver has no temporal smoothing, FocusTrackingModel publishes without removeDuplicates, and SuggestionCoordinator routes every Blocked snapshot to OverlayController.hide -> panel.orderOut, so each flicker tore down and rebuilt the overlay several times per second, lagging input. Add a tiny pure FocusCapabilityFlickerGate that swallows the first Blocked snapshot on the same elementIdentifier after a Supported one; a second consecutive Blocked still propagates so genuine focus loss is not delayed. Wire it into handleFocusSnapshotChange and log the disabledReason on both the change and suppression paths so future log dives can attribute which Blocked branch fired.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Apple Calendar's event-editor field momentarily drops one of the required AX attributes during its own redraws, which collapsed
FocusCapabilityResolverto Blocked for a single poll (~13 ms) and bounced back to Supported on the next. Each flip droveSuggestionCoordinatorto hide and re-show the overlay, so users saw the suggestion UI flickering several times per second and felt input lag whenever they typed in a Calendar event.This adds a small pure
FocusCapabilityFlickerGatethat swallows the firstSupported → Blockedtransition on the sameelementIdentifier; a second consecutive Blocked read still propagates so genuine focus loss is not delayed.Validation
Verified the root cause from
~/Library/Logs/Cotabby/cotabby.jsonl: samecom.apple.iCal-38455-…element flipsBlocked → Supportedin 12-13 ms pairs every ~750 ms, with the same code path producing 6 such pairs across a 30 s Calendar window vs. 2 total for Xcode.xcodebuild testfails locally with a code-signing / Team ID mismatch on the test bundle (the known issue called out in.claude/CLAUDE.md); the newFocusCapabilityFlickerGateTestscover first-snapshot, single-flicker suppression, return-to-Supported reset, two-consecutive-Blocked release, different-element bypass, no-prior-Supported passthrough, and Unsupported clear-state.Linked issues
None filed. Repro: focus the title or notes field of a new Calendar event, watch overlay open and close repeatedly and ghost text fail to keep up.
Risk / rollout notes
Supported → Blockededge. Different element, prior Unsupported, and the second consecutive Blocked read all pass through unchanged, so legitimate focus loss is delayed by at most one additional poll (~80-150 ms in practice).reason=to theFocus snapshot changedandFocus snapshot flicker suppressedlog lines so the next investigation can attribute the exact Blocked branch (selection-length vs. secure-field vs. missing capability) without re-instrumenting.project.pbxprojregenerated viaxcodegen generatefromproject.ymlto pick up the two new files.Greptile Summary
This PR introduces
FocusCapabilityFlickerGate, a small pure struct that suppresses transientSupported → Blocked → SupportedAX capability flicker on the same focused element, preventingSuggestionCoordinatorfrom tearing down and rebuilding the suggestion overlay on every ~750 ms AX redraw cycle in Apple Calendar's event editor.FocusCapabilityFlickerGate: Tracks the last delivered supported element ID and a consecutive-blocked counter; suppresses the first blocked read on the same element and propagates the second, capping suppression latency at roughly one extra poll interval (~80–150 ms).SuggestionCoordinator+Input: GateshandleFocusSnapshotChangewith the new evaluator before any overlay mutation, with trace logging for suppressed events and an addeddetail=field for the passed-through path.Confidence Score: 5/5
Safe to merge; the gate is a pure value type with no side effects, all non-flicker paths are unchanged, and genuine focus loss is delayed by at most one poll cycle.
The change is self-contained — a small struct with clearly bounded state (two fields), an exhaustive switch over a three-case enum, and nine unit tests that cover every branch including nil-context edge cases. The integration in the coordinator is a single early-return guard; all existing downstream logic is unaffected. There are no mutations of shared state, no async coordination, and no new dependencies introduced.
No files require special attention.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[FocusSnapshot arrives] --> B{capabilityFlickerGate.evaluate} B --> |.supported| C[Store elementID\nReset counter\nReturn .apply] B --> |.unsupported| D[Clear state\nReturn .apply] B --> |.blocked| E{Same element\nas last supported?} E --> |No / nil context| F[Clear state\nReturn .apply] E --> |Yes| G[Increment\nconsecutiveBlockedCount] G --> H{count >= 2?} H --> |Yes| I[Clear state\nReturn .apply] H --> |No| J[Return .suppress\npendingBlockedReadCount=count] C --> K[handleFocusSnapshotChange\ncontinues normally] D --> K F --> K I --> K J --> L[Log suppression\nReturn early — overlay unchanged]Reviews (2): Last reviewed commit: "Address Greptile review: detail= log key..." | Re-trigger Greptile