Files
01d28384e5 fix(paste): wait for held modifier keys before injecting a paste on Linux (#2207)
* fix(paste): wait for held modifier keys before injecting a paste on Linux

A paste or selection-copy chord injected while the user still physically
holds a modifier reaches the target as a different shortcut (Ctrl+Super+V,
Shift+Ctrl+Insert), so the transcript silently never lands. Push-to-talk
ends on the first key of a chord to come up, and a tap hotkey can still be
down when a fast transcript arrives, so this is routine rather than rare.

Releasing the keys for the user, as windows-fast-paste does, cannot work
on Linux: the kernel drops a key-up from a virtual device that never
pressed that key. Instead linux-fast-paste gains
--await-modifier-release, which polls XKB base modifiers on X11 or
EVIOCGKEY on Wayland until the keys are up. Every Linux paste tool and
the Linux selection capture wait on it once. Keys still held after 1.5s
leave the text on the clipboard and surface the dictation-error pill
instead of injecting blind. Without /dev/input access on Wayland the
state is unknown and pasting behaves as before.

macOS needs no change: its CGEvent paste sets explicit flags, and a
modifier held at the HID level does not leak into the posted event.

* fix(review): iteration 1 — 1 issue (held-key capture disposition)

A selection capture that times out on held modifiers now runs the voice command in the assistant panel, like accessibility_unavailable, instead of failing it as a selection edit with a misleading permissions message.

* fix(paste): Measure the modifier wait on the clock and keep held-back edits

The helper counted 10 ms sleeps instead of reading a clock, so under load
it reported less time than had passed and could run past the JS watchdog,
which kills it at timeout + 1 s and pastes anyway into the held key. Time
the wait on CLOCK_MONOTONIC; the native fixture now checks the reported
wait against wall-clock time.

A selection edit blocked by held keys surfaced as "selection_unavailable"
or "paste_failed", so the pill blamed permissions or hid that the edit was
on the clipboard. Report "modifiers_held" from replaceSelectedText at both
points and route it through the shared held-back pill, with its own copy in
every locale.

Retry on a held-back pill now pastes the kept transcript again instead of
starting a new recording, and the hook keeps the pill in processing until
the paste attempt settles, since the audio manager settles before the paste
starts and the wait can take 1.5 s.

Add tests for the watchdog and spawn-error branches under frozen timers,
drop the explicit undefined `reason` from safePaste, and assert the capture
returned when the wait passes.

* fix(paste): Ignore ydotoold and report unreadable modifier state

* fix(paste): Choose the paste target after the modifier wait

The wait for held keys can last up to 1.5 s, long enough for focus to
move. pasteLinux classified the target window before waiting, so the
chord could suit one window (Ctrl+V) and reach another (a terminal).
The wait now runs before target detection.

The Linux selection copy approves its target before the wait, so a copy
that had to wait now looks the target up again and reports
target_changed if focus moved, instead of sending a plain Ctrl+C into a
terminal. _awaitModifierRelease resolves { state, waitedMs } so a copy
that did not wait skips the extra lookup.

* fix(paste): Ignore repeat Retry clicks and leave newer pills alone

Retry on a held-back pill re-pastes the kept transcript, which can wait
up to 1.5 s on held keys with no visible progress. The action has no
pending state, so a second click queued a second paste and the text
landed twice. A retry whose paste landed after a newer pill or recording
also dismissed that newer pill.

Retry now ignores clicks while its paste is pending and dismisses the
pill only while it is still the current one.

* fix(paste): Keep dictation hotkeys working while a paste is pending

The hook reported "processing" to main for the whole paste attempt so
the pill would not sit idle during the Linux modifier wait. Main drops
dictation hotkeys while processing, so a press during any paste (up to
1.5 s on Linux, a few hundred ms elsewhere) was silently ignored.

The pill still shows processing, but main is no longer told, so hotkeys
behave as they did before this branch. A paste still waiting when the
next recording starts no longer paints that recording as processing,
and if it is then held back, the transcript stays on the clipboard
without a pill: the new recording would dismiss the pill at once, and
hiding the preview would hide the new recording's. Overlapping pastes
are counted, so the first to settle no longer clears the pending state.

* fix(paste): Run a voice command when focus moves during the modifier wait

A Linux selection capture that had to wait for held keys looks the
target up again and declines if focus moved, so the copy chord never
reaches an unchecked window. At capture time that decline surfaced as
target_changed, which is fatal when the dictation agent is reachable:
holding the voice assistant hotkey's modifier and switching windows
dropped the command with a "target changed" pill.

The post-wait move now carries code focus_moved, which the capture
disposition treats as standalone, so the command runs in the panel.
Revalidating a selection edit or caret session still declines as
target_changed.

* fix(paste): Keep the force-stop pill's Retry recording again

Held-back pills gained a Retry that re-pastes the kept transcript, and
the push force-stop pill got it too. Force stops only happen on macOS
and Windows, which do not wait for held modifiers, so clicking Retry
while the push keys were still down injected the paste chord into them,
which the pill exists to prevent.

Only modifiers-held pills re-paste on Retry now; the force-stop pill
records again, as before this branch.

---------

Co-authored-by: boseq <307062491+boseq@users.noreply.github.com>
Co-authored-by: Chadpiha <chadpiha23@gmail.com>
2026-10-01 00:13:45 +02:00
..