Output: restore the clipboard only after the paste has read it - #66
Merged
Merged
Conversation
The transcript goes on the pasteboard as a promise, so the target app's read comes back as a call. The old clipboard returns 400 ms after Cmd+V as before if the transcript has been read by then, otherwise 200 ms after the read, or after 8 s if nothing reads it. An app busy when Cmd+V arrives reads late, and the fixed 400 ms alone handed it the user's old clipboard: a page blocked for a second got the old clipboard 12 of 12 times on main and the transcript 11 of 12 times here. The twelfth is Chromium reading once at Cmd+V and again at the paste, which main fails too; a read never shortens the 400 ms for that reason. The promise is served on the main thread, so the time from Cmd+V to the first read is logged in the new `paste` category. prepare() no longer snapshots the clipboard while our own transcript is still on it, which would read our own promise from off the main thread. The clipboard-only path without Accessibility is unchanged. Refs #40 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CLAUDE.md's Output row and clipboard risk, PERFORMANCE.md's restore line, and the new `paste` log lines in BENCHMARKS.md. Refs #40 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
Experiment 3 from #40. The clipboard restore now waits for the target app to read the transcript, not only for a clock.
What changes
PasteboardOutputwrites the transcript as a pasteboard promise (TranscriptPromise), with the same nspasteboard.org marker types as before. The target app's read comes back to Pladder as a call, and the old clipboard returns:A read never brings the clipboard back sooner than 400 ms. That differs from Handy's scheme (200 ms after the last read) on purpose: Chromium sometimes reads once as Cmd+V arrives and again when the page gets round to the paste, and the pasteboard serves the second read itself, so it never reaches us. Restoring 200 ms after the first read handed such a page the old clipboard.
The clipboard-only path without Accessibility and the send key's Return are unchanged. A new
pastelog category records when the target read the transcript, measured from Cmd+V.Why
With the fixed 400 ms, an app whose main thread is busy when Cmd+V arrives reads the pasteboard after the restore and pastes the user's old clipboard. Measured on #40 with a prototype, and below with this branch's
PasteboardOutputitself.Correctness: the real
PasteboardOutput, main vs this branchA throwaway harness (not committed) drove
prepare()andinsert()into windows it opened itself, then selected all, copied, and checked what had landed. The busy page blocks its main thread for a second on Cmd+V, standing in for a heavy web or Electron app. M1, macOS 27.0.The one miss is the first paste into a freshly opened Helium page: Chromium read at 13 ms, then pasted a second later. It is the case the 400 ms floor exists for; main fails it too. Normal targets read 1 to 25 ms after Cmd+V (from the new log line).
Release-to-paste
The write is on the release-to-paste path, so per CLAUDE.md, before and after. The stage it changes is
paste(TextOutput.insert): 30 pastes into TextEdit per run, alternating builds.No difference beyond noise. The engine is untouched, so
pladder-cli benchwas not rerun.Trade-offs
clipboard read … after Cmd+Vline shows it if not. That time comes afterrelease-to-paste, which ends when Cmd+V is posted.pboardhanding the change touseractivityd, i.e. Universal Clipboard) reads every unmarked write about 15 ms after it lands, paste or not. With the markers, nothing read early in any check.prepare()was the only such reader and now skips its snapshot while our transcript is on the pasteboard. The harness hit exactly this before it was fixed.Tests
swift test: all pass. New: the restore deadline rule (prompt read, late read, no read, read near the cap, read before Cmd+V), and a promise on a private named pasteboard serves the text with the markers, reports the read and round-trips a restore.In the bundled app, with real dictation
scripts/bundle.shbuild of this branch, launched as a test copy with its own settings file and hotkey (F19), dictating withsaythrough the speakers into the microphone, into windows the driver opened itself:release-to-paste0.179 to 0.221 s withpasteat 1 to 2 ms;clipboard read5 to 25 ms after Cmd+V for normal targets and about 1.01 to 1.04 s for the busy pages, so Pladder's main thread was never what the target waited for.Not done
Refs #40
🤖 Generated with Claude Code