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
4 changes: 4 additions & 0 deletions Fluid.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand All @@ -36,6 +37,7 @@
7C91B0022F42AA0100C0DEF0 /* HotkeyShortcutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyShortcutTests.swift; sourceTree = "<group>"; };
7CDB0A292F3C4D5600FB7CAD /* DictationE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictationE2ETests.swift; sourceTree = "<group>"; };
343B29013F4441D6A797D12D /* LLMClientRequestBodyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMClientRequestBodyTests.swift; sourceTree = "<group>"; };
7CFA1D0B2F500000C0DEF002 /* TypingServiceTransientPasteboardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingServiceTransientPasteboardTests.swift; sourceTree = "<group>"; };
7CDB0A2A2F3C4D5600FB7CAD /* AudioFixtureLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFixtureLoader.swift; sourceTree = "<group>"; };
7CDB0A2B2F3C4D5600FB7CAD /* dictation_fixture.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = dictation_fixture.wav; sourceTree = "<group>"; };
7CDB0A2C2F3C4D5600FB7CAD /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
Expand Down Expand Up @@ -107,6 +109,7 @@
7CDB0A292F3C4D5600FB7CAD /* DictationE2ETests.swift */,
7C91B0022F42AA0100C0DEF0 /* HotkeyShortcutTests.swift */,
343B29013F4441D6A797D12D /* LLMClientRequestBodyTests.swift */,
7CFA1D0B2F500000C0DEF002 /* TypingServiceTransientPasteboardTests.swift */,
);
path = FluidDictationIntegrationTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -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;
};
Expand Down
17 changes: 16 additions & 1 deletion Sources/Fluid/Services/TypingService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
)
}
}
Loading