Skip to content

Commit :emoji: on Tab even while a suggestion overlay is visible#458

Merged
FuJacob merged 1 commit into
mainfrom
fix/emoji-tab-commit-while-suggestion-visible
May 31, 2026
Merged

Commit :emoji: on Tab even while a suggestion overlay is visible#458
FuJacob merged 1 commit into
mainfrom
fix/emoji-tab-commit-while-suggestion-visible

Conversation

@FuJacob

@FuJacob FuJacob commented May 31, 2026

Copy link
Copy Markdown
Owner

Summary

The inline :emoji: picker would ignore the first Tab and only start working on later attempts. Root cause: the emoji commit fires from the listen-only observer pass (EmojiPickerController.observe()machine.reduce(.commitKey)); the consuming accept tap only swallows the key afterward. But InputMonitor.routeObserverKeyDown suppressed the accept key (returned before calling onEvent) whenever isAcceptTapOwningAcceptKeys was true, which is the case whenever a ghost suggestion is concurrently visible. So if a late async suggestion became visible while the emoji panel was open (most likely on the slow first generation after focusing a field), Tab was classified as .acceptance, never reached the emoji controller, and was routed to the suggestion-accept path instead. Once the suggestion cleared, the emoji committed normally, which is why it "worked afterwards."

Fix: exclude an open emoji capture from accept-key recognition at the observer, so the emoji picker keeps its intended "first look at every keystroke" even with a suggestion overlay showing:

let recognizesAcceptance = isAcceptTapOwningAcceptKeys && !captureInterceptionActive

The consuming tap's emojiCaptureKeyDecider then swallows the Tab (the head-inserted observer runs before the tail-appended accept tap, so pendingDecision is already set to consume). When no capture is open, captureInterceptionActive is false and behavior is identical to before.

Also adds the previously-missing EmojiPickerController logging (capture open / abort-on-nil-context / commit / focus-change cancel / replace) so this path is diagnosable from cotabby.jsonl, and a regression test.

Validation

swiftlint lint --quiet   # exit 0 on changed files
xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -configuration Debug \
  -destination 'platform=macOS' build              # ** BUILD SUCCEEDED **
xcodebuild ... build-for-testing                   # ** TEST BUILD SUCCEEDED **

The app-hosted unit-test run could not execute locally due to the known Team ID signing mismatch (test bundle signed "Sign to Run Locally" vs the host app's Apple Development identity → dlopen rejects the bundle). build-for-testing confirms the new test compiles; CI runs the suite with consistent signing.

New test: test_observerTapRoutesAcceptKeyToEmojiObserverWhileCapturingDespiteVisibleSuggestion asserts that with both captureInterceptionActive and isAcceptTapOwningAcceptKeys set, the accept key reaches onEvent and is not classified as .acceptance.

Linked issues

(none filed)

Risk / rollout notes

  • The diagnosis was adversarially verified against the code (the suppression mechanism, the async ownership flip, and the fix's tap-ordering/no-regression properties all confirmed). The one thing not proven from logs is that the suggestion-visible race is exactly what the user hit, because EmojiPickerController had no logging until this PR. A second candidate cause exists: on the very first : after focusing a field, the AX focus context can still be nil, so beginCapture aborts and no panel opens. The added logging now distinguishes the two; if reports persist, check cotabby.jsonl for "emoji capture aborted at open".
  • InputMonitor.captureInterceptionActive widened from private to internal (mirrors isAcceptTapOwningAcceptKeys) so the test can stage state without installing real CGEvent taps. Production still mutates it only via setCaptureInterceptionActive(_:).

Greptile Summary

This PR fixes a race condition where pressing Tab to commit an inline :emoji: would silently misfire on the first attempt whenever a ghost suggestion happened to be visible concurrently. The single-line logic fix in routeObserverKeyDown (recognizesAcceptance = isAcceptTapOwningAcceptKeys && !captureInterceptionActive) ensures the accept key still flows through to the emoji controller's observer pass even when the suggestion overlay is also claiming the accept key.

  • InputMonitor.swift: The core fix — excludes an active emoji capture from accept-key suppression at the observer, so EmojiPickerController.observe() always gets "first look" at the commit key. captureInterceptionActive is widened to internal (matching the existing isAcceptTapOwningAcceptKeys pattern) so tests can stage the concurrent-ownership state without installing real CGEvent taps.
  • EmojiPickerController.swift: Adds import Logging and five debug log points (aborted open, capture opened, commit skipped/proceeding, focus-change cancel, replace operation) so the emoji commit path is now fully observable from cotabby.jsonl.
  • InputMonitorTests.swift: Adds a targeted regression test that stages both flags (captureInterceptionActive = true, isAcceptTapOwningAcceptKeys = true) and verifies Tab reaches onEvent and is not classified as .acceptance.

Confidence Score: 5/5

Safe to merge — the change is a minimal, well-scoped single-expression fix with a direct regression test, and no existing behavior is altered when capture is not active.

The fix is exactly one boolean expression change in routeObserverKeyDown. All three combinations of the two flags produce correct routing. Logging is purely additive.

No files require special attention.

Important Files Changed

Filename Overview
Cotabby/Services/Input/InputMonitor.swift Core fix: adds && !captureInterceptionActive to recognizesAcceptance so Tab is not silently dropped when both emoji capture and a ghost suggestion are active simultaneously.
Cotabby/App/Coordinators/EmojiPickerController.swift Adds import Logging and targeted debug-log lines at five previously-silent points. No logic changes.
CotabbyTests/InputMonitorTests.swift Adds regression test staging both captureInterceptionActive = true and isAcceptTapOwningAcceptKeys = true, verifying Tab reaches onEvent and is not classified as .acceptance.

Reviews (1): Last reviewed commit: "Commit :emoji: on Tab even while a sugge..." | Re-trigger Greptile

@FuJacob FuJacob merged commit dc2f366 into main May 31, 2026
4 checks passed
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