From 3979f64bd1b2ba43eab89ea52ed4cb69b5b3507b Mon Sep 17 00:00:00 2001 From: postoso Date: Sat, 4 Jul 2026 22:13:01 -0400 Subject: [PATCH] fix: exclude temporary paste writes from clipboard manager history The clipboard-paste insertion path writes the transcription to NSPasteboard.general to drive a synthetic Cmd+V, then restores the previous clipboard immediately after. Clipboard managers (Maccy, Raycast, Paste, etc.) record every pasteboard change, so that temporary write lands in history even though it is reverted right away. With a clipboard manager running, every dictation that uses the paste path leaves a transcription entry in history. Mark the temporary write with the nspasteboard.org TransientType and AutoGeneratedType marker types so well-behaved clipboard managers skip it. The change is confined to withTemporaryPasteboardString, which every clipboard-paste path funnels through; the string content and restore logic are untouched, so paste behavior and clipboard restoration are unchanged. ConcealedType is deliberately not used, since it signals sensitive content. --- Fluid.xcodeproj/project.pbxproj | 4 ++ Sources/Fluid/Services/TypingService.swift | 17 ++++++- ...ypingServiceTransientPasteboardTests.swift | 51 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 Tests/FluidDictationIntegrationTests/TypingServiceTransientPasteboardTests.swift diff --git a/Fluid.xcodeproj/project.pbxproj b/Fluid.xcodeproj/project.pbxproj index 0725e76d..d5404a49 100644 --- a/Fluid.xcodeproj/project.pbxproj +++ b/Fluid.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 7CDB0A2D2F3C4D5600FB7CAD /* DictationE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDB0A292F3C4D5600FB7CAD /* DictationE2ETests.swift */; }; 7CDB0A2E2F3C4D5600FB7CAD /* AudioFixtureLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDB0A2A2F3C4D5600FB7CAD /* AudioFixtureLoader.swift */; }; 86CAA2D4EF18433096185602 /* LLMClientRequestBodyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 343B29013F4441D6A797D12D /* LLMClientRequestBodyTests.swift */; }; + 7CFA1D0B2F500000C0DEF001 /* TypingServiceTransientPasteboardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFA1D0B2F500000C0DEF002 /* TypingServiceTransientPasteboardTests.swift */; }; 7CDB0A2F2F3C4D5600FB7CAD /* dictation_fixture.wav in Resources */ = {isa = PBXBuildFile; fileRef = 7CDB0A2B2F3C4D5600FB7CAD /* dictation_fixture.wav */; }; 7CDB0A302F3C4D5600FB7CAD /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7CDB0A2C2F3C4D5600FB7CAD /* XCTest.framework */; }; 7CE006BD2E80EBE600DDCCD6 /* AppUpdater in Frameworks */ = {isa = PBXBuildFile; productRef = 7CE006BC2E80EBE600DDCCD6 /* AppUpdater */; }; @@ -36,6 +37,7 @@ 7C91B0022F42AA0100C0DEF0 /* HotkeyShortcutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyShortcutTests.swift; sourceTree = ""; }; 7CDB0A292F3C4D5600FB7CAD /* DictationE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictationE2ETests.swift; sourceTree = ""; }; 343B29013F4441D6A797D12D /* LLMClientRequestBodyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMClientRequestBodyTests.swift; sourceTree = ""; }; + 7CFA1D0B2F500000C0DEF002 /* TypingServiceTransientPasteboardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingServiceTransientPasteboardTests.swift; sourceTree = ""; }; 7CDB0A2A2F3C4D5600FB7CAD /* AudioFixtureLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFixtureLoader.swift; sourceTree = ""; }; 7CDB0A2B2F3C4D5600FB7CAD /* dictation_fixture.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = dictation_fixture.wav; sourceTree = ""; }; 7CDB0A2C2F3C4D5600FB7CAD /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; @@ -107,6 +109,7 @@ 7CDB0A292F3C4D5600FB7CAD /* DictationE2ETests.swift */, 7C91B0022F42AA0100C0DEF0 /* HotkeyShortcutTests.swift */, 343B29013F4441D6A797D12D /* LLMClientRequestBodyTests.swift */, + 7CFA1D0B2F500000C0DEF002 /* TypingServiceTransientPasteboardTests.swift */, ); path = FluidDictationIntegrationTests; sourceTree = ""; @@ -262,6 +265,7 @@ 7CDB0A2D2F3C4D5600FB7CAD /* DictationE2ETests.swift in Sources */, 7C91B0012F42AA0100C0DEF0 /* HotkeyShortcutTests.swift in Sources */, 86CAA2D4EF18433096185602 /* LLMClientRequestBodyTests.swift in Sources */, + 7CFA1D0B2F500000C0DEF001 /* TypingServiceTransientPasteboardTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/Fluid/Services/TypingService.swift b/Sources/Fluid/Services/TypingService.swift index c52c4de6..9202d849 100644 --- a/Sources/Fluid/Services/TypingService.swift +++ b/Sources/Fluid/Services/TypingService.swift @@ -570,6 +570,18 @@ final class TypingService { _ = pasteboard.writeObjects(restoredItems) } + /// Builds the pasteboard item for a temporary paste write, tagged with the nspasteboard.org + /// Transient and AutoGenerated marker types so clipboard managers exclude it from history. + /// `ConcealedType` is deliberately not used: it signals sensitive/password content, which + /// would be misleading for a dictation transcript. + static func makeTransientPasteboardItem(_ text: String) -> NSPasteboardItem { + let item = NSPasteboardItem() + item.setString(text, forType: .string) + item.setData(Data(), forType: NSPasteboard.PasteboardType("org.nspasteboard.TransientType")) + item.setData(Data(), forType: NSPasteboard.PasteboardType("org.nspasteboard.AutoGeneratedType")) + return item + } + private func withTemporaryPasteboardString( _ text: String, restoreDelayMicros: useconds_t, @@ -587,7 +599,10 @@ final class TypingService { let snapshot = self.capturePasteboardSnapshot(pasteboard) pasteboard.clearContents() - guard pasteboard.setString(text, forType: .string) else { + // Mark this temporary write so clipboard managers (Maccy, Raycast, Paste, etc.) skip it. + // The string is written only to drive a synthetic Cmd+V and the previous clipboard is + // restored immediately afterward, so it is ephemeral, not something the user copied. + guard pasteboard.writeObjects([Self.makeTransientPasteboardItem(text)]) else { self.log("[TypingService] ERROR: Failed to set temporary clipboard string") self.restorePasteboardSnapshot(snapshot, to: pasteboard) return false diff --git a/Tests/FluidDictationIntegrationTests/TypingServiceTransientPasteboardTests.swift b/Tests/FluidDictationIntegrationTests/TypingServiceTransientPasteboardTests.swift new file mode 100644 index 00000000..1fb1374a --- /dev/null +++ b/Tests/FluidDictationIntegrationTests/TypingServiceTransientPasteboardTests.swift @@ -0,0 +1,51 @@ +import AppKit +@testable import FluidVoice_Debug +import XCTest + +// The temporary clipboard write used to drive synthetic Cmd+V must be tagged so clipboard +// managers exclude it from history. It is restored immediately and is not user-copied content. +final class TypingServiceTransientPasteboardTests: XCTestCase { + + func testTransientPasteboardItem_carriesText() { + let item = TypingService.makeTransientPasteboardItem("hello world") + XCTAssertEqual( + item.string(forType: .string), + "hello world", + "the plain string must survive so paste and clipboard restore behave unchanged" + ) + } + + func testTransientPasteboardItem_isMarkedForClipboardManagers() { + let item = TypingService.makeTransientPasteboardItem("hello world") + let transientType = NSPasteboard.PasteboardType("org.nspasteboard.TransientType") + let autoGeneratedType = NSPasteboard.PasteboardType("org.nspasteboard.AutoGeneratedType") + let types = item.types.map(\.rawValue) + XCTAssertTrue( + types.contains(transientType.rawValue), + "temporary write must be marked Transient so clipboard managers skip it" + ) + XCTAssertTrue( + types.contains(autoGeneratedType.rawValue), + "temporary write must be marked AutoGenerated so clipboard managers skip it" + ) + XCTAssertEqual( + item.data(forType: transientType)?.count, + 0, + "TransientType should be a marker-only zero-length payload" + ) + XCTAssertEqual( + item.data(forType: autoGeneratedType)?.count, + 0, + "AutoGeneratedType should be a marker-only zero-length payload" + ) + } + + func testTransientPasteboardItem_isNotMarkedConcealed() { + let item = TypingService.makeTransientPasteboardItem("hello world") + let types = item.types.map(\.rawValue) + XCTAssertFalse( + types.contains("org.nspasteboard.ConcealedType"), + "ConcealedType signals sensitive content and must not be applied to a transcript" + ) + } +}