docs/plans/2026-04-15-buttoncore-left-click-safety-design.md
Re-scope ButtonCore so the default runtime path no longer places real primary mouse button events inside a global active CGEventTap. Preserve synthetic left-click mapping and drag behavior, but stop mutating or consuming real leftMouseDown/leftMouseUp events by default.
The current ButtonCore path installs a single .defaultTap interceptor over:
leftMouseDownleftMouseUprightMouseDownrightMouseUpotherMouseDownotherMouseUpkeyDownkeyUpThat active tap both matches bindings and injects virtual modifier flags into passthrough keyboard and mouse events. User testing showed a critical incompatibility: merely including real leftMouseDown in the active tap can leave some applications in a half-committed UI state even when the callback returns the original event unchanged.
There are three distinct left-click-related flows:
ButtonCoreShortcutExecutorMouseInteractionSessionControllerOnly the first path requires touching real leftMouseDown/leftMouseUp. The second and third paths already operate on synthetic events or drag/move rewrite paths and do not require active interception of the user's real primary click.
ButtonCore into safe active dispatch plus passive primary observationReplace the single eventInterceptor with two interceptors:
dispatchInterceptor
.defaultTapkeyDownkeyUpotherMouseDownotherMouseUpprimaryObservationInterceptor
.listenOnlyleftMouseDownleftMouseUprightMouseDownrightMouseUpThe passive observer exists only if we still want runtime visibility or future diagnostics around primary-button events. It must not consume events or mutate flags.
Keep virtual modifier propagation for keyboard passthrough in ButtonCore, but stop mutating real physical mouse down/up events by default.
This means:
ShortcutExecutorMouseInteractionSessionControllerMouseInteractionSessionController should remain responsible for:
This path is not implicated by the current bug report and should remain the single owner of drag rewriting.
Today the codebase allows modifier-plus-primary-click recording. That creates a product/runtime mismatch if the default runtime no longer modifies real primary clicks.
For this iteration, the design chooses safety over capability:
Add regression coverage for:
ButtonCore mask excludes left/right mouse down/upIf advanced users still need modifier-plus-real-left-click support, add a dedicated isolated interceptor path later with all of these properties:
That future work should not block the safe-default fix.