From 0effbaadcff7d29e648aae8b9cb205d1ca63ead3 Mon Sep 17 00:00:00 2001 From: Ali Alavi Date: Sun, 23 Aug 2026 21:32:41 +0000 Subject: [PATCH] fix(mac): separate passive state sync from active voice dictation and force replace --- mac/src/RelayClient.swift | 47 ++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/mac/src/RelayClient.swift b/mac/src/RelayClient.swift index 361a184..1866b27 100644 --- a/mac/src/RelayClient.swift +++ b/mac/src/RelayClient.swift @@ -17,8 +17,13 @@ public final class RelayClient: NSObject, URLSessionWebSocketDelegate { private var monitorTimerSource: DispatchSourceTimer? private let monitorQueue = DispatchQueue(label: "com.soniox.macsync.monitor", qos: .userInteractive) + // Callback: (text, cursor, isFullReplace) public var onRemoteUpdateReceived: ((String, Int?, Bool) -> Void)? + // Dedup guard: prevent rapid duplicate keystrokes within 200ms + private var lastInjectedText: String = "" + private var lastInjectedTime: Double = 0 + override private init() { super.init() let config = URLSessionConfiguration.default @@ -72,7 +77,7 @@ public final class RelayClient: NSObject, URLSessionWebSocketDelegate { case .success(let message): self.isConnected = true switch message { - case .string(let text): + case .string(let text): self.handleMessageString(text) case .data(let data): if let text = String(data: data, encoding: .utf8) { @@ -97,7 +102,7 @@ public final class RelayClient: NSObject, URLSessionWebSocketDelegate { return } - let action = json["action"] as? String ?? json["type"] as? String + let action = json["action"] as? String ?? json["type"] as? String ?? "" let source = json["source"] as? String ?? "" // Ignore echo messages originated by Mac itself @@ -106,13 +111,43 @@ public final class RelayClient: NSObject, URLSessionWebSocketDelegate { let updateText = json["text"] as? String ?? json["insert_text"] as? String ?? "" let cursor = json["cursor"] as? Int ?? json["cursor_pos"] as? Int - if action == "paste" || action == "set_text" || action == "update_input" || action == "phone_input_edit" || action == "sync_state" || action == "insert_speech" { - let isFullReplace = (action == "update_input" || action == "set_text" || action == "force_replace") - print("RelayClient: ⚡ Received remote update: '\(updateText.prefix(30))...' (replace: \(isFullReplace), cursor: \(cursor ?? -1))") + let now = Date().timeIntervalSince1970 + + // 1. Live Voice Dictation from Phone Mic: Paste ONLY new speech at active cursor (Cmd+V) + if action == "insert_speech" || action == "speech_insert" { + guard !updateText.isEmpty else { return } + + // Dedup exact duplicate within 250ms + if updateText == lastInjectedText && (now - lastInjectedTime) < 0.25 { + return + } + lastInjectedText = updateText + lastInjectedTime = now + + print("RelayClient: 🎙️ Voice Dictation Paste: '\(updateText.prefix(30))...' (Cmd+V at cursor)") DispatchQueue.main.async { - self.onRemoteUpdateReceived?(updateText, cursor, isFullReplace) + self.onRemoteUpdateReceived?(updateText, cursor, false) } } + + // 2. Explicit "Send to Mac" Button on Phone: Full Replace (Cmd+A + Cmd+V) + else if action == "update_input" || action == "force_replace" || action == "paste" { + guard !updateText.isEmpty else { return } + + lastInjectedText = updateText + lastInjectedTime = now + + print("RelayClient: 🚀 Force Replace All: '\(updateText.prefix(30))...' (Cmd+A + Cmd+V)") + DispatchQueue.main.async { + self.onRemoteUpdateReceived?(updateText, cursor, true) + } + } + + // 3. Passive State Synchronization (typing on phone keyboard): + // Updates room state cache only — DOES NOT trigger keystrokes on Mac! + else if action == "sync_state" || action == "phone_input_edit" { + // Passive sync only + } } private func startInputMonitoring() {