feat: Paste Last Transcription (clipboard-free) via hotkey + overlay#488
Conversation
…oard-free Re-inserts the most recent transcription into the focused text field using the existing clipboard-free insertion path (TypingService standard mode). Unlike "Copy Last Transcription", it never writes to the system clipboard, so it works for users who keep `copyTranscriptionToClipboard` disabled. Useful when auto-insert drops the tail of a dictation or text lands in the wrong field. Exposed two ways, both routed through ContentView.pasteLastDictationFromHistory(): - A configurable global hotkey, unbound and disabled by default (opt-in, assigned in Settings, mirroring the Command/Edit-mode default-off model). GlobalHotkeyManager reads the shortcut live from SettingsStore, like Cancel. - An item in the overlay actions menu beside "Copy Last Transcription". The action pastes the last entry's processedText (fallback rawText) verbatim: no new history entry, no reformatting. It reuses resolveTypingTargetPID() / restoreFocusToRecordingTarget() so it targets the right field for both the hotkey (insert at cursor) and menu (restore prior focus) cases, is ignored while recording, and no-ops with a debug log when history is empty. The new shortcut + enabled flag are persisted and added to SettingsBackupPayload as optionals (older backups still decode). Adds HotkeyShortcutTests coverage for the default (unbound/disabled) and persistence/clear round-trip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1bd57dbaa2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| let processed = last.processedText.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| let raw = last.rawText.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| let text = processed.isEmpty ? raw : processed |
There was a problem hiding this comment.
Preserve stored whitespace when re-pasting history
When continuous dictation spacing is enabled, ASRService.applyContinuousDictationFormatting deliberately saves boundary whitespace in processedText (for example a leading space before a word and a trailing space after it), but this new paste path trims both processedText and rawText before insertion. In that setting, re-pasting the latest entry no longer reproduces the text that was originally delivered and can concatenate words at the cursor, which contradicts the clipboard-free “faithful re-paste” behavior.
Useful? React with 👍 / 👎.
| // Check the "paste last transcription" shortcut (a one-shot action, like cancel). | ||
| if SettingsStore.shared.pasteLastTranscriptionShortcutEnabled, | ||
| let pasteShortcut = SettingsStore.shared.pasteLastTranscriptionHotkeyShortcut, | ||
| pasteShortcut.matches(keyCode: keyCode, modifiers: eventModifiers) | ||
| { | ||
| self.triggerPasteLastTranscription() | ||
| return nil |
There was a problem hiding this comment.
Debounce repeated keyDown events for paste shortcut
This hotkey fires on every matching keyDown without tracking the press until keyUp or checking keyboardEventAutorepeat, unlike the recording-mode shortcuts below that guard with is...KeyPressed. If the user holds the configured paste shortcut long enough for macOS key repeat, the callback can run repeatedly and insert multiple copies of the same transcription while the physical shortcut is still down.
Useful? React with 👍 / 👎.
|
I am working on this as well! Would love ypur help here!! :)). If you can send me a recording of it working and the settings page where you set the shortcut, I would much appreciate it and it would be easy for me to merge it. I just need to make sure that UI and UX looks okay and good before merging, other than that - amazing work |
Two issues found while testing the feature on a real build: 1. Shortcut recording gave no feedback for opt-in (off-by-default) rows. In `shortcutRow`, the "press keys…" capture pill was gated on `isRecording && enabledValue`, so for a row whose toggle is still off (Command Mode, Paste Last Transcription) clicking "Change" started recording but kept showing "Not set" — it looked dead. Show the capture pill whenever recording, regardless of the enable toggle. 2. The paste hotkey fired on key-down while its own modifiers (e.g. ⌘⌃) were still physically held, so the synthesized characters were interpreted by the target app as modifier+key shortcuts and dropped (the insert "succeeded" but no text landed). Dictation never hits this because no keys are held by the time it types. Paste now waits (up to 0.6s, polling 15ms) for the modifier keys to be released before inserting, so the text lands cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5fc1ad7826
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // Check the "paste last transcription" shortcut (a one-shot action, like cancel). | ||
| if SettingsStore.shared.pasteLastTranscriptionShortcutEnabled, | ||
| let pasteShortcut = SettingsStore.shared.pasteLastTranscriptionHotkeyShortcut, | ||
| pasteShortcut.matches(keyCode: keyCode, modifiers: eventModifiers) | ||
| { |
There was a problem hiding this comment.
Route modifier-only paste shortcuts through flagsChanged
The settings recorder accepts modifier-only shortcuts for this target (it creates a HotkeyShortcut from handleShortcutFlagsChangedEvent and then assigns .pasteLast), but the new paste action is only checked in the .keyDown path here. If a user records a modifier-only shortcut such as Right Option or Ctrl+Shift, macOS delivers it as .flagsChanged and the existing flagsChanged handlers cover the other shortcut types but not paste, so Settings can show the shortcut as enabled even though it never triggers.
Useful? React with 👍 / 👎.
|
I updated the PR, There are a couple small issues, but it's all working now. I attached a video. I absolutely love this product from the sounds To the AI enhancement. This is by far the best mac app STT out there. Keep up the good work |
Addresses issues found in a self-review of the feature: - Abort instead of inserting if the modifier keys never release. The wait helper now returns whether the modifiers were released; if they're still held when the 0.6s timeout elapses (stuck/long-held key), the paste is aborted rather than typing a corrupted — possibly destructive (⌘W/⌘Q) — shortcut sequence. The user can simply retrigger. - Ignore auto-repeat key-downs so holding the chord pastes once, not N times. The auto-repeat state is passed to triggerPasteLastTranscription, which now acts only on the initial press. (Passed as an argument rather than branching in the already-at-limit handleKeyEvent.) - Guard the overlay entry point against pasting mid-recording. The asrService .isRunning check now lives inside pasteLastDictationFromHistory (re-checked after the modifier wait), so the overlay menu item is protected like the hotkey path and the trigger/insert TOCTOU window is closed. - Restore backup symmetrically: the paste shortcut is now `if let`-guarded like its enabled flag, so restoring an older backup can't wipe a configured shortcut while leaving the feature flagged enabled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 08ba816869
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if typingTarget.shouldRestoreOriginalFocus { | ||
| await self.restoreFocusToRecordingTarget() |
There was a problem hiding this comment.
Avoid restoring stale focus for paste hotkey
When the paste action is invoked by the global hotkey while the user is still in the same app as the last dictation but has moved the cursor to a different field/window, resolveTypingTargetPID() returns the current PID with shouldRestoreOriginalFocus == true because it only compares against recordingTargetPID; this branch then calls restoreFocusToRecordingTarget(), which refocuses the old AX element captured at recording start. In that scenario the hotkey inserts into the previous dictation field rather than the currently focused field, so focus restoration should be reserved for the overlay/menu path or only when Fluid is frontmost.
Useful? React with 👍 / 👎.
|
Perfect - Once you fix the issues - I am merging this blindly ;) |
The earlier "abort if modifiers still held" guard timed out at 0.6s, so a
second/held press of the paste hotkey aborted with the transcript never
inserting ("only pastes once"). Make the wait generous (5s) so the paste
lands when the keys are released — including a held or quick repeated press —
and only abort if a modifier is genuinely stuck. Preserves the protection
against synthesizing text while modifiers are down.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Because the paste waits for the modifier keys to release, a quick double/triple tap of the chord queued several Tasks that all inserted at once on release, duplicating the transcription. Guard with a single in-flight flag (set before the wait, cleared in a defer) so only one paste runs at a time, covering both the hotkey and overlay entry points. Deliberate sequential pastes (press, it lands, press again) still work since the flag clears when the paste completes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
✅ Ready to merge — everything's working and testedSince the demo video I did a full pass of manual testing plus a self‑review and fixed everything I found. The feature works end‑to‑end and is verified on a signed local build: assign a shortcut in Settings → Shortcuts, dictate, then re‑paste the last transcription into the focused field with the clipboard never touched. Bugs found and fixed during testing/review:
Quality: passes One known limitation (noted in code): with macOS Sticky Keys, a latched modifier means inserting would corrupt the text, so the paste aborts safely rather than typing garbage. Happy to squash to a single commit on merge if you'd prefer. Thanks again |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 92e66dccb4
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if let pasteLastTranscriptionHotkeyShortcut = payload.pasteLastTranscriptionHotkeyShortcut { | ||
| self.pasteLastTranscriptionHotkeyShortcut = pasteLastTranscriptionHotkeyShortcut | ||
| } |
There was a problem hiding this comment.
Restore cleared paste shortcuts from backups
When restoring a backup created after this change with Paste Last Transcription left unbound, the synthesized encoder omits pasteLastTranscriptionHotkeyShortcut because it is nil, so this if let treats that current backup the same as a pre-feature backup and leaves any existing shortcut in UserDefaults. A user restoring an unbound/disabled configuration over a machine that previously had a paste shortcut will still see the old binding available to re-enable, so backup restore no longer faithfully restores this setting; the restore path needs a way to distinguish missing legacy fields from an intentionally nil shortcut or otherwise clear it for current backups.
Useful? React with 👍 / 👎.
| @State private var pasteLastTranscriptionHotkeyShortcut: HotkeyShortcut? = SettingsStore.shared.pasteLastTranscriptionHotkeyShortcut | ||
| @State private var isPasteLastTranscriptionShortcutEnabled: Bool = SettingsStore.shared.pasteLastTranscriptionShortcutEnabled |
There was a problem hiding this comment.
Refresh paste shortcut state after backup restore
These new @State values are only initialized once, but reloadSettingsStateAfterBackupRestore() refreshes the other hotkey bindings after .settingsBackupDidRestore and does not refresh the paste shortcut or enabled flag. If a backup changes this shortcut while Preferences is open, the global hotkey manager reads the restored values from SettingsStore but the Settings UI keeps showing the old binding/toggle and can mislead the user; include these two states in the restore reload alongside the other shortcut states.
Useful? React with 👍 / 👎.
|
One final comment - does it reuse the same gotkey flow from the other items? Makin sure you can use all forms of shortcut for this like kyeboard, mouse etc just like the other ones! Thanks |
|
Great question — yes, it reuses the existing shortcut machinery end‑to‑end, nothing bespoke. "Paste Last Transcription" goes through the same On shortcut types, it matches the other secondary actions exactly:
So it behaves "just like the other ones" (the non‑primary ones). If you'd actually like to open mouse buttons (or modifier‑only) up to the secondary actions app‑wide, that's a small tweak to |
Makes Paste Last as flexible as the Primary Dictation ("listen") button —
mappable to a mouse button (side buttons, modified clicks), not just the
keyboard. Adds .pasteLast to allowsMouseShortcut and wires a mouse-button
check into GlobalHotkeyManager: the mouse-down handling is extracted into
handleMouseShortcutDown/Up helpers (which also keeps handleKeyEvent under the
cyclomatic-complexity limit), the paste fires once on mouse-down (one-shot,
mirroring the keyboard path), and the paired mouse-up is swallowed so the
focused app never sees an orphaned up. The other secondary actions
(Command/Edit/Cancel) remain keyboard-only as before. Adds a test that the
paste setting round-trips a mouse shortcut.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Done — just pushed mouse‑button support for Paste Last ( Implementation notes:
Builds clean, passes |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3b2dfe787f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if SettingsStore.shared.pasteLastTranscriptionShortcutEnabled, | ||
| let pasteShortcut = SettingsStore.shared.pasteLastTranscriptionHotkeyShortcut, | ||
| pasteShortcut.isMouseShortcut, | ||
| pasteShortcut.mouseButton == mouseButton | ||
| { | ||
| return true |
There was a problem hiding this comment.
Track paste mouse-down before swallowing mouse-up
Because this branch ignores both the release modifiers and whether the preceding mouse-down actually matched Paste Last, it also consumes releases for other uses of the same button. For example, if Primary Dictation is Ctrl+Mouse 4 and Paste Last is Option+Mouse 4 (a non-duplicate combination the conflict checker allows), the primary mouse-down starts recording but the mouse-up returns here before finishPrimaryShortcutPress(.mouse(...)), so hold/automatic mode can leave dictation running until the user stops it another way.
Useful? React with 👍 / 👎.
Demo
CleanShot.2026-06-30.at.11.40.47.mp4
Shows assigning the shortcut in Settings → Shortcuts and re-pasting the last transcription into a focused field (clipboard untouched).
Description
Adds a "Paste Last Transcription" action that re-inserts the most recent transcription into the focused text field using FluidVoice's existing clipboard-free insertion path (
TypingServicestandard mode). Unlike "Copy Last Transcription," it never writes to the system clipboard, so it works for users who intentionally keepCopy transcription to clipboarddisabled. Useful when auto-insert drops the tail of a dictation or the text lands in the wrong place.It's exposed two ways, both routed through a single
ContentView.pasteLastDictationFromHistory():The action re-inserts the last entry's
processedText(falling back torawText), creates no new history entry, and does no reformatting — it's a faithful re-paste. It reusesresolveTypingTargetPID()/restoreFocusToRecordingTarget()so it targets the right field in both the hotkey case (insert at the current cursor) and the menu case (restore focus to the prior app first). It's ignored while a recording is in progress, and no-ops with a debug log when history is empty.Type of Change
Related Issues
Testing
swiftlint --strict --config .swiftlint.yml Sourcesswiftformat --config .swiftformat SourcesAdded
HotkeyShortcutTestscases covering the new setting's default (unbound + disabled) and persistence/clear round-trip.Two fixes landed after exercising the feature on a real build:
shortcutRow): the "press keys…" capture pill was gated onisRecording && enabledValue, so for an opt-in row whose toggle is still off, clicking "Change" started recording but kept showing "Not set" — it looked unresponsive. Now the pill shows whenever recording. Note: this also improves the existing Command Mode row (same helper), which had the same first-record blind spot — flagging in case that's unexpected.Notes
SettingsStore(like the Cancel shortcut), so there's no extra plumbing through theGlobalHotkeyManagerinitializer.SettingsBackupPayloadas optionals, so older backup files still decode.textInsertionMode:standardinserts with no clipboard involvement;reliablePasteuses the existing temporary-clipboard-then-restore mechanism. The independentcopyTranscriptionToClipboardsetting is untouched.Screenshots / Video
See the Demo at the top — it walks through assigning the shortcut in Settings and re-pasting the last transcription into a focused field with the clipboard untouched.