From c78a70066788c338b07e17a20e4bebcc9d837533 Mon Sep 17 00:00:00 2001 From: zahirulAIIUB Date: Tue, 30 Jun 2026 08:20:07 +0600 Subject: [PATCH 1/5] feat(settings): add Clipboard Only text insertion mode (#481) Adds a new `clipboardOnly` case to `TextInsertionMode`. When selected, dictated text is written to the system clipboard without any keyboard or accessibility-API insertion. This avoids the accessibility-permission requirement entirely. Intended for remote desktop workflows (e.g. Sunshine/Moonlight) where a synced clipboard carries the text to the remote machine, and for users who prefer to paste manually rather than have FluidVoice type directly. The mode appears in the Text Insertion Mode picker alongside the existing Clipboard Free Insert and Clipboard Paste options. --- Sources/Fluid/Persistence/SettingsStore.swift | 5 +++++ Sources/Fluid/Services/TypingService.swift | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/Sources/Fluid/Persistence/SettingsStore.swift b/Sources/Fluid/Persistence/SettingsStore.swift index 82d0fbec..fb8859dc 100644 --- a/Sources/Fluid/Persistence/SettingsStore.swift +++ b/Sources/Fluid/Persistence/SettingsStore.swift @@ -4467,6 +4467,7 @@ extension SettingsStore { enum TextInsertionMode: String, CaseIterable, Identifiable, Codable { case standard case reliablePaste + case clipboardOnly var id: String { self.rawValue @@ -4478,6 +4479,8 @@ extension SettingsStore { return "Clipboard Free Insert" case .reliablePaste: return "Clipboard Paste" + case .clipboardOnly: + return "Clipboard Only" } } @@ -4487,6 +4490,8 @@ extension SettingsStore { return "Fastest path. Inserts text without changing the clipboard, with paste fallback if direct insertion is unavailable." case .reliablePaste: return "Compatibility path. Uses a temporary clipboard paste, so clipboard history apps may briefly record dictated text." + case .clipboardOnly: + return "Copies transcribed text to the clipboard without inserting it. Useful for remote desktop workflows or when you prefer to paste manually." } } } diff --git a/Sources/Fluid/Services/TypingService.swift b/Sources/Fluid/Services/TypingService.swift index 259c9a05..1c4791d6 100644 --- a/Sources/Fluid/Services/TypingService.swift +++ b/Sources/Fluid/Services/TypingService.swift @@ -277,6 +277,14 @@ final class TypingService { return } + // Clipboard-only mode: skip all insertion machinery and write to clipboard directly. + // This avoids the accessibility-permission requirement and settle delay entirely. (#481) + if mode == .clipboardOnly { + self.bench("request_return reason=clipboard_only") + ClipboardService.copyToClipboard(text) + return + } + // Prevent concurrent typing operations guard !self.isCurrentlyTyping else { self.bench("request_return reason=already_typing") From 75cd207efdb10f081e83073314a7248e1b5ce6e6 Mon Sep 17 00:00:00 2001 From: zahirulAIIUB Date: Tue, 30 Jun 2026 08:41:39 +0600 Subject: [PATCH 2/5] fix(settings): route clipboardOnly through clipboard delivery, not typed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the main dictation output path, `shouldTypeExternally` was true for all modes when Fluid wasn't frontmost, so `clipboardOnly` still called `typeTextToActiveField` and set `didTypeExternally = true` — causing the `outputDelivered` event to fire with `AnalyticsOutputMethod.typed` instead of `.clipboard`, and `markTranscriptionCompleted` to record a typed delivery. Now `clipboardOnly` mode is included in `shouldCopyToClipboard` (so clipboard delivery and `.clipboard` analytics always fire for this mode) and excluded from `shouldTypeExternally` (so no insertion and no `.typed` analytics). The `historyOnly` fallback branch uses `!shouldCopyToClipboard` instead of re-reading `copyTranscriptionToClipboard == false` to avoid double-counting when either clipboard setting triggered delivery. --- Sources/Fluid/ContentView.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/Fluid/ContentView.swift b/Sources/Fluid/ContentView.swift index 1a5d3ec3..6814c75b 100644 --- a/Sources/Fluid/ContentView.swift +++ b/Sources/Fluid/ContentView.swift @@ -2288,9 +2288,10 @@ struct ContentView: View { // When FluidVoice itself is frontmost, the bound editor already receives `finalText`. // Avoid re-inserting or overwriting the clipboard in that self-target case. + let isClipboardOnlyMode = SettingsStore.shared.textInsertionMode == .clipboardOnly let shouldCopyToClipboard = shouldPersistOutputs && - SettingsStore.shared.copyTranscriptionToClipboard && - !isFluidFrontmost + !isFluidFrontmost && + (SettingsStore.shared.copyTranscriptionToClipboard || isClipboardOnlyMode) if shouldCopyToClipboard { ClipboardService.copyToClipboard(finalText) @@ -2304,7 +2305,7 @@ struct ContentView: View { } var didTypeExternally = false - let shouldTypeExternally = shouldPersistOutputs && !isFluidFrontmost + let shouldTypeExternally = shouldPersistOutputs && !isFluidFrontmost && !isClipboardOnlyMode DebugLogger.shared.debug( "Typing decision → frontmost: \(frontmostName), fluidFrontmost: \(isFluidFrontmost), editorFocused: \(self.isTranscriptionFocused), willTypeExternally: \(shouldTypeExternally)", @@ -2354,7 +2355,7 @@ struct ContentView: View { NotchOverlayManager.shared.hide() } } else if shouldPersistOutputs, - SettingsStore.shared.copyTranscriptionToClipboard == false, + !shouldCopyToClipboard, SettingsStore.shared.saveTranscriptionHistory { AnalyticsService.shared.capture( From 0e94c18ba28ee6f998cbca69f4536bc1b336a062 Mon Sep 17 00:00:00 2001 From: zahirulAIIUB Date: Tue, 30 Jun 2026 10:39:45 +0600 Subject: [PATCH 3/5] feat(settings): clarify Clipboard Only mode label to 'Copy to Clipboard Only' --- Sources/Fluid/Persistence/SettingsStore.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Fluid/Persistence/SettingsStore.swift b/Sources/Fluid/Persistence/SettingsStore.swift index fb8859dc..6adeb491 100644 --- a/Sources/Fluid/Persistence/SettingsStore.swift +++ b/Sources/Fluid/Persistence/SettingsStore.swift @@ -4480,7 +4480,7 @@ extension SettingsStore { case .reliablePaste: return "Clipboard Paste" case .clipboardOnly: - return "Clipboard Only" + return "Copy to Clipboard Only" } } From 1d88b08faa2ef07359a35b5cbb389ef2d17bf319 Mon Sep 17 00:00:00 2001 From: zahirulAIIUB Date: Tue, 30 Jun 2026 10:53:15 +0600 Subject: [PATCH 4/5] fix(clipboard): deliver clipboard-only output when frontmost and label rewrite analytics - Clipboard-only is the primary delivery method, so it must write to the pasteboard even when FluidVoice is frontmost; the previous !isFluidFrontmost guard suppressed the only delivery in that case. - Voice-rewrite and accept-rewrite paths now report .clipboard (not .typed) analytics in clipboard-only mode, matching what actually happens. --- Sources/Fluid/ContentView.swift | 17 +++++++++++++---- Sources/Fluid/Services/RewriteModeService.swift | 7 ++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Sources/Fluid/ContentView.swift b/Sources/Fluid/ContentView.swift index 6814c75b..f1c89f2d 100644 --- a/Sources/Fluid/ContentView.swift +++ b/Sources/Fluid/ContentView.swift @@ -2289,9 +2289,13 @@ struct ContentView: View { // When FluidVoice itself is frontmost, the bound editor already receives `finalText`. // Avoid re-inserting or overwriting the clipboard in that self-target case. let isClipboardOnlyMode = SettingsStore.shared.textInsertionMode == .clipboardOnly + // Clipboard-only is the primary delivery method, so it must write even when FluidVoice is + // frontmost — suppressing it there would leave the user with nothing on the clipboard. The + // backup-copy checkbox keeps its `!isFluidFrontmost` guard, since the bound editor already + // holds the text in that self-target case. let shouldCopyToClipboard = shouldPersistOutputs && - !isFluidFrontmost && - (SettingsStore.shared.copyTranscriptionToClipboard || isClipboardOnlyMode) + (isClipboardOnlyMode || + (!isFluidFrontmost && SettingsStore.shared.copyTranscriptionToClipboard)) if shouldCopyToClipboard { ClipboardService.copyToClipboard(finalText) @@ -2679,7 +2683,10 @@ struct ContentView: View { ) } - // Type the rewritten text + // Deliver the rewritten text. In clipboard-only mode the insertion machinery writes to + // the pasteboard instead of typing, so report the matching analytics method rather than + // unconditionally claiming a typed insertion. (#481) + let isClipboardOnlyMode = SettingsStore.shared.textInsertionMode == .clipboardOnly let typingTarget = self.resolveTypingTargetPID() if typingTarget.shouldRestoreOriginalFocus { await self.restoreFocusToRecordingTarget() @@ -2692,7 +2699,9 @@ struct ContentView: View { .outputDelivered, properties: [ "mode": AnalyticsMode.rewrite.rawValue, - "method": AnalyticsOutputMethod.typed.rawValue, + "method": isClipboardOnlyMode + ? AnalyticsOutputMethod.clipboard.rawValue + : AnalyticsOutputMethod.typed.rawValue, ] ) diff --git a/Sources/Fluid/Services/RewriteModeService.swift b/Sources/Fluid/Services/RewriteModeService.swift index 0ac9ee25..16c0f9a0 100644 --- a/Sources/Fluid/Services/RewriteModeService.swift +++ b/Sources/Fluid/Services/RewriteModeService.swift @@ -164,11 +164,16 @@ final class RewriteModeService: ObservableObject { NSApp.hide(nil) // Restore focus to the previous app self.typingService.typeTextInstantly(self.rewrittenText) + // Clipboard-only mode routes the delivery to the pasteboard instead of typing, so report the + // method that actually happened rather than always claiming a typed insertion. (#481) + let isClipboardOnlyMode = SettingsStore.shared.textInsertionMode == .clipboardOnly AnalyticsService.shared.capture( .outputDelivered, properties: [ "mode": AnalyticsMode.rewrite.rawValue, - "method": AnalyticsOutputMethod.typed.rawValue, + "method": isClipboardOnlyMode + ? AnalyticsOutputMethod.clipboard.rawValue + : AnalyticsOutputMethod.typed.rawValue, ] ) } From 075fbc578e2079519d4ac1bc20082da12a712bd7 Mon Sep 17 00:00:00 2001 From: zahirulAIIUB Date: Tue, 30 Jun 2026 11:06:09 +0600 Subject: [PATCH 5/5] fix(clipboard): collapse clipboard-only rewrite delivery into a single event In clipboard-only mode the rewrite delivery already writes the pasteboard and emits one .clipboard event, so skip the backup-copy block to avoid emitting a second clipboard delivery event when the backup setting is also on. Matches the dictation path, which collapses these into a single event. --- Sources/Fluid/ContentView.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/Fluid/ContentView.swift b/Sources/Fluid/ContentView.swift index f1c89f2d..25f5bdf2 100644 --- a/Sources/Fluid/ContentView.swift +++ b/Sources/Fluid/ContentView.swift @@ -2671,8 +2671,13 @@ struct ContentView: View { if !self.rewriteModeService.rewrittenText.isEmpty { DebugLogger.shared.info("Rewrite successful, typing result (chars: \(self.rewriteModeService.rewrittenText.count))", source: "ContentView") + // In clipboard-only mode the delivery below already writes the pasteboard and emits a + // single `.clipboard` event, so skip the backup copy to avoid double-counting that + // delivery — mirroring how the dictation path collapses these into one event. (#481) + let isClipboardOnlyMode = SettingsStore.shared.textInsertionMode == .clipboardOnly + // Copy to clipboard as backup - if SettingsStore.shared.copyTranscriptionToClipboard { + if SettingsStore.shared.copyTranscriptionToClipboard, !isClipboardOnlyMode { ClipboardService.copyToClipboard(self.rewriteModeService.rewrittenText) AnalyticsService.shared.capture( .outputDelivered, @@ -2686,7 +2691,6 @@ struct ContentView: View { // Deliver the rewritten text. In clipboard-only mode the insertion machinery writes to // the pasteboard instead of typing, so report the matching analytics method rather than // unconditionally claiming a typed insertion. (#481) - let isClipboardOnlyMode = SettingsStore.shared.textInsertionMode == .clipboardOnly let typingTarget = self.resolveTypingTargetPID() if typingTarget.shouldRestoreOriginalFocus { await self.restoreFocusToRecordingTarget()