Cap pasteboard-hold to 200ms when paste verification has no AX target#292
Open
NathanDrake2406 wants to merge 1 commit intoaltic-dev:mainfrom
Open
Cap pasteboard-hold to 200ms when paste verification has no AX target#292NathanDrake2406 wants to merge 1 commit intoaltic-dev:mainfrom
NathanDrake2406 wants to merge 1 commit intoaltic-dev:mainfrom
Conversation
When withTemporaryPasteboardString dispatches a paste into an app that exposes no accessible text element, waitForFocusedTextVerification falls through to an unconditional usleep(timeoutMicros). All three clipboard entry points pass 5_000_000, so the session semaphore is held for 5 seconds on Electron apps, web-based editors, many terminals, and games. A second dictation triggered during that window blocks for up to 5s waiting on pasteboardSessionSemaphore. timeoutMicros is sized for AX verification polling, which cannot run when no snapshot exists. The degenerate branch reused the verification budget as a pasteboard-hold budget, which is the wrong invariant: verification needs seconds of headroom for polling, while the pasteboard hold only needs enough time for the target to consume Cmd+V before restore. Introduce pasteboardReadWindowMicros (200_000) and cap the nil-snapshot wait to min(timeoutMicros, pasteboardReadWindowMicros). The verified path and all four PasteVerificationResult values are unchanged. A unit test would either mirror the patch constant or mock captureFocusedTextSnapshot, both of which restate the implementation. Verified manually by pasting into an Electron target and immediately retriggering dictation; the second trigger no longer blocks.
There was a problem hiding this comment.
Pull request overview
This PR reduces how long TypingService holds the pasteboard session after dispatching Cmd+V when there’s no focused AX text snapshot to verify the paste against, preventing long (multi-second) blocking of subsequent dictation-triggered paste operations in non-AX-friendly targets (e.g., many Electron apps).
Changes:
- Introduces a
pasteboardReadWindowMicrosceiling (200ms) for the nil-snapshot (“unverifiable”) paste verification path. - Uses
min(timeoutMicros, pasteboardReadWindowMicros)so call sites with smaller timeouts still apply.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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.
What this changes
Caps the pasteboard-hold after Cmd+V to 200ms when
waitForFocusedTextVerificationhas no AX snapshot to verify against. The three clipboard entry points still pass5_000_000as the verified-path ceiling; the new cap only applies to the nil-snapshot branch.Why
Every clipboard paste into an app that exposes no accessible text element (Electron apps, web-based editors, many GPU terminals, games) hits the nil-snapshot branch in
TypingService.waitForFocusedTextVerificationatSources/Fluid/Services/TypingService.swift:942. That branch runs an unconditionalusleep(timeoutMicros), and each of the three call sites (insertTextViaClipboardToPid,insertTextViaClipboard,insertTextViaMenuPaste) hardcodes5_000_000. The session semaphore is transferred topasteboardRestoreQueueand held for the full 5 seconds. If the user triggers a second dictation during that window,withTemporaryPasteboardStringblocks onpasteboardSessionSemaphore.wait()for up to 5 seconds with no visible cause.Approach
The degenerate branch was conflating two different budgets under one
timeoutMicrosparameter:Introduce
pasteboardReadWindowMicros = 200_000as the ceiling for the non-verified wait, and usemin(timeoutMicros, pasteboardReadWindowMicros)so any future call site that passes a smaller value still wins.Non-goals: the verified path, the polling cadence, and the AppleScript snapshot cost for Xcode/Notes are untouched. Those are separate issues (see follow-ups).
Validation
Manual: paste into an Electron target (VS Code, Discord, Slack, Spotify), then immediately retrigger dictation. Before: second trigger blocks visibly for several seconds. After: second trigger proceeds without delay.
No unit test added. Any such test would either hard-code the new
200_000literal (structure mirror) or mockcaptureFocusedTextSnapshotto force a nil return and assert onusleepduration (mocks an internal collaborator). The real signal lives at the AX + NSPasteboard + target-app boundary, which the test environment cannot simulate.Risks / follow-ups
captureXcodeScriptSnapshoton every 50ms iteration, which issues two blocking AppleScript round-trips. Effective poll interval for Xcode is 200 to 600ms, not 50ms. A cheap-AX-first / expensive-AppleScript-last split would fix this.