Fix accept tap race#391
Open
FuJacob wants to merge 1 commit into
Open
Conversation
Comment on lines
+354
to
+356
| DispatchQueue.main.async { | ||
| _ = onEvent(capturedEvent) | ||
| } |
Contributor
There was a problem hiding this comment.
DispatchQueue.main.async is used from an @MainActor-isolated method. While this works because DispatchQueue.main and @MainActor share the same underlying thread, it bypasses Swift's concurrency type-system guarantees and can produce warnings under strict concurrency checks. Task { @MainActor in … } is the idiomatic form here and makes the isolation explicit.
Suggested change
| DispatchQueue.main.async { | |
| _ = onEvent(capturedEvent) | |
| } | |
| Task { @MainActor in | |
| _ = onEvent(capturedEvent) | |
| } |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
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
Fixes the accept-key race by making the active consuming tap own acceptance delivery instead of relying on the listen-only observer to run first. The observer now ignores accept keys, and consumed accept events carry an explicit replay flag so rejected or failed accepts can safely pass Tab back to the host app without double-replaying observed keys.
Validation
xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' build** BUILD SUCCEEDED **xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' build-for-testing** TEST BUILD SUCCEEDED **xcodebuild test -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' -only-testing:CotabbyTests/SuggestionStateHelperTests -only-testing:CotabbyTests/SuggestionSessionReconcilerTestsCotabbyTests.xctestbundle and host process had different Team IDs.Linked issues
None.
Risk / rollout notes
This changes accept-key ownership in the event-tap pipeline. Manual smoke testing should cover partial accept, final-chunk accept, stale-session pass-through, and browser/editor Tab behavior.
Greptile Summary
This PR restructures the accept-key event pipeline so the active consuming tap (not the listen-only observer) is the single source of truth for acceptance delivery. The observer now silently skips accept keys, the accept tap creates the
CapturedInputEventwithreplaysWhenAcceptanceRejected: trueand dispatchesonEventasynchronously, and all rejection paths inacceptSuggestionhave been updated to replay the consumed key back to the host app.InputMonitor.swift: Removes thependingObserverAcceptedKeyEvent/isHandlingAcceptKeyObserverEvent/ deferred-teardown machinery; observer tap early-returns on accept keys; accept tap now constructs the event and callsonEventviaDispatchQueue.main.asyncbefore returningnil.InputModels.swift: AddsreplaysWhenAcceptanceRejectedfield (defaultfalse) andisAcceptanceKeyEventhelper toCapturedInputEvent.SuggestionCoordinator+Acceptance.swift: GuardsreplayConsumedAcceptKeycalls behindreplay.replaysWhenAcceptanceRejected; adds replay in the insert-failed path; removesstate == .readygate fromshouldConsumeAcceptKeyProvider.Confidence Score: 3/5
Safe on the happy path, but the early-exit in handleInputEvent for disabled reasons does not replay the consumed Tab, which breaks the replay contract introduced by this PR.
All rejection branches inside acceptSuggestion correctly call passTabThrough(replay:) or replayConsumedAcceptKey directly. However, handleInputEvent has a prior early-exit — the disabledReason check — that bypasses acceptSuggestion entirely and returns false without checking event.replaysWhenAcceptanceRejected. If that branch runs for an event the accept tap already consumed (because the coordinator's disabled state changed between the tap callback and the async dispatch), the user's Tab is silently dropped. The timing window is narrow, but the code path is real and the omission is inconsistent with the rest of the replay design.
SuggestionCoordinator+Input.swift — the disabledReason early-exit at the top of handleInputEvent needs a replay check to match the pattern used everywhere else in acceptSuggestion.
Important Files Changed
Sequence Diagram
sequenceDiagram participant HID as HID Event System participant ObsTap as Observer Tap (listenOnly, head) participant AccTap as Accept Tap (defaultTap, tail) participant Main as Main Queue (async) participant Coord as SuggestionCoordinator participant HostApp as Host App HID->>ObsTap: keyDown (Tab) Note over ObsTap: isAcceptanceKeyEvent → skip onEvent ObsTap-->>HID: passUnretained (not consumed) HID->>AccTap: keyDown (Tab) AccTap->>AccTap: shouldConsumeAcceptKeyProvider()? alt session active and overlay visible AccTap->>Main: "async { onEvent(capturedEvent) }" AccTap-->>HID: nil (consumed) Main->>Coord: "onEvent [replaysWhenAcceptanceRejected=true]" alt acceptance succeeds Coord->>Coord: insertSuggestion / advanceSession else acceptance rejected Coord->>HostApp: synthesized Tab replayed end else no valid session AccTap-->>HID: passUnretained HID->>HostApp: Tab delivered normally endReviews (1): Last reviewed commit: "Fix accept tap race" | Re-trigger Greptile