Skip to content

Fix text injection landing nowhere: post to cgAnnotatedSessionEventTap - #27

Closed
imtamiliniyan wants to merge 1 commit into
humanitas-labs:masterfrom
imtamiliniyan:master
Closed

imtamiliniyan wants to merge 1 commit into
humanitas-labs:masterfrom
imtamiliniyan:master

Conversation

@imtamiliniyan

Copy link
Copy Markdown

TextInjector posts synthetic keystrokes to .cgSessionEventTap — the same event-pipeline location where HotkeyMonitor installs its own listen-only tap (.headInsertEventTap on .cgSessionEventTap) to detect the fn hotkey. The injected keyDown/keyUp events collide with that tap and get silently dropped before ever reaching the focused application. Transcription and hotkey detection both work fine — nothing ever gets typed anywhere, in any app, regardless of focus.

Confirmed empirically with a standalone harness posting the same event via all three CGEventTapLocation options: cghidEventTap and cgSessionEventTap both silently no-op; cgAnnotatedSessionEventTap reliably lands in the focused text field.

Fixes/relates to #13, #17.

TextInjector posted synthetic keystrokes to .cgSessionEventTap, the same
event-pipeline location HotkeyMonitor installs its listen-only tap at
(.headInsertEventTap on .cgSessionEventTap). The synthetic keyDown/keyUp
events collided with that tap and were silently dropped before reaching
the focused application - transcription and the fn-hotkey worked fine,
but nothing ever got typed anywhere.

Confirmed empirically with a standalone harness posting the same event
via all three CGEventTapLocation options: cghidEventTap and
cgSessionEventTap both no-op, cgAnnotatedSessionEventTap reliably lands
in the focused text field.

Related upstream: humanitas-labs#13, humanitas-labs#17
FernandoGomes83 added a commit to FernandoGomes83/parrot that referenced this pull request Aug 26, 2026
…e-unicode fallback path

Post synthetic keystrokes to cgAnnotatedSessionEventTap so our own
hotkey tap doesn't swallow them, and stop setting the unicode string
on the key-up event, which duplicated the text in some apps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJSwEJLoPGF23gRToanmHJ
@dremnik

dremnik commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Thanks for this. A lot of the open PRs overlap, so rather than merging them one at a time I'm rewriting the fixes against a single plan. Your work is credited in #38, which now tracks the scope, and the commits that build on it will reference this PR. Closing in favour of that issue.

@dremnik dremnik closed this Sep 27, 2026
birhantprkc pushed a commit to birhantprkc/parrot that referenced this pull request Sep 28, 2026
TextInjector put the Unicode payload on both key-down and key-up, so some apps inserted every chunk twice. Terminals and Electron apps ignore CGEventKeyboardSetUnicodeString, so dictation there produced nothing and no error. Events had a nil source and inherited whatever modifiers the user held, so a held Control could turn text into shortcuts. And the injector never checked focus: a click during transcription sent the text to the wrong field, possibly a password field.

Paste is now the default: the injector snapshots every representation of every pasteboard item, writes the transcript marked with org.nspasteboard.TransientType and ConcealedType so clipboard managers skip it, posts ⌘V, and restores the snapshot after 250 ms. A second paste inside that window keeps the first snapshot, so the user's own clipboard always comes back, and a copy made by anything else during the window is kept rather than overwritten. Paste works everywhere paste works, including the terminals and editors where type-unicode drops text silently, which is worth borrowing the clipboard for 250 ms. --inject-mode type-unicode keeps the old path for anyone who wants the clipboard left alone, now with the payload on key-down only. Every synthesized event comes from a private CGEventSource with explicit flags.

Events still post at cgSessionEventTap. humanitas-labs#27 reported that location dropping events and cgAnnotatedSessionEventTap working, but our tap is listen-only and cannot swallow anything, so that explanation does not hold and there is no confirmed reason to move. The location is one constant, to be confirmed against the app matrix on hardware.

At recording start the controller takes a FocusSnapshot: the frontmost app, its focused element, and whether that element is a secure text field or reports protected content. Before delivery a pure decision compares it with a fresh snapshot. A secure field at either end discards the transcript, neither typed nor copied, and logs why. A different app or element puts the transcript on the clipboard instead, and the overlay says so through a UserFacingError. The decision and the pasteboard session are tested, the latter also against a real private pasteboard.

Thanks to humanitas-labs#24 (@justinhnaylor) for the key-down-only fix; humanitas-labs#27 (@imtamiliniyan) for the report that injection lands nowhere and the harness comparing tap locations; humanitas-labs#4 (@Entrepenulian) for paste mode with a full multi-representation snapshot, the guard against overlapping restores, ⌘V on keycode 9 with explicit flags, and the case for paste as the default; humanitas-labs#26 (@willmather95) for the secure-field check, the focus snapshot with a clipboard fallback, and the private event source; and @raffanegro for reporting dropped injections.

Fixes humanitas-labs#38
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