Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions Sources/Fluid/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2288,9 +2288,14 @@ 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 &&
SettingsStore.shared.copyTranscriptionToClipboard &&
!isFluidFrontmost
(isClipboardOnlyMode ||
(!isFluidFrontmost && SettingsStore.shared.copyTranscriptionToClipboard))

if shouldCopyToClipboard {
ClipboardService.copyToClipboard(finalText)
Expand All @@ -2304,7 +2309,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)",
Expand Down Expand Up @@ -2354,7 +2359,7 @@ struct ContentView: View {
NotchOverlayManager.shared.hide()
}
} else if shouldPersistOutputs,
SettingsStore.shared.copyTranscriptionToClipboard == false,
!shouldCopyToClipboard,
SettingsStore.shared.saveTranscriptionHistory
{
AnalyticsService.shared.capture(
Expand Down Expand Up @@ -2666,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,
Expand All @@ -2678,7 +2688,9 @@ 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 typingTarget = self.resolveTypingTargetPID()
if typingTarget.shouldRestoreOriginalFocus {
await self.restoreFocusToRecordingTarget()
Expand All @@ -2691,7 +2703,9 @@ struct ContentView: View {
.outputDelivered,
properties: [
"mode": AnalyticsMode.rewrite.rawValue,
"method": AnalyticsOutputMethod.typed.rawValue,
"method": isClipboardOnlyMode
? AnalyticsOutputMethod.clipboard.rawValue
: AnalyticsOutputMethod.typed.rawValue,
Comment thread
devzahirul marked this conversation as resolved.
]
)

Expand Down
5 changes: 5 additions & 0 deletions Sources/Fluid/Persistence/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4467,6 +4467,7 @@ extension SettingsStore {
enum TextInsertionMode: String, CaseIterable, Identifiable, Codable {
case standard
case reliablePaste
case clipboardOnly

var id: String {
self.rawValue
Expand All @@ -4478,6 +4479,8 @@ extension SettingsStore {
return "Clipboard Free Insert"
case .reliablePaste:
return "Clipboard Paste"
case .clipboardOnly:
return "Copy to Clipboard Only"
}
}

Expand All @@ -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."
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion Sources/Fluid/Services/RewriteModeService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]
)
}
Expand Down
8 changes: 8 additions & 0 deletions Sources/Fluid/Services/TypingService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
devzahirul marked this conversation as resolved.
Comment thread
devzahirul marked this conversation as resolved.
Comment thread
devzahirul marked this conversation as resolved.
}

// Prevent concurrent typing operations
guard !self.isCurrentlyTyping else {
self.bench("request_return reason=already_typing")
Expand Down
Loading