From a03620f9c12b31900a1dbb49d04e7e23f7841360 Mon Sep 17 00:00:00 2001 From: Harshith Pasupuleti Date: Sun, 22 Mar 2026 22:50:32 +0530 Subject: [PATCH 1/2] i --- .../Features/Clipboard/ClipboardManager.swift | 112 +++++++++++++----- 1 file changed, 85 insertions(+), 27 deletions(-) diff --git a/dhavnii/Features/Clipboard/ClipboardManager.swift b/dhavnii/Features/Clipboard/ClipboardManager.swift index 73801e8..96ef47f 100644 --- a/dhavnii/Features/Clipboard/ClipboardManager.swift +++ b/dhavnii/Features/Clipboard/ClipboardManager.swift @@ -11,6 +11,25 @@ import Carbon /// Manages clipboard operations and auto-pasting class ClipboardManager { + private let autoPasteRetryDelay: TimeInterval = 0.12 + private let autoPasteMaxAttempts = 10 + + private struct FocusedElementState { + let bundleIdentifier: String + let role: String? + let isEditable: Bool + + var isValidPasteTarget: Bool { + switch role { + case "AXTextField", "AXTextArea", "AXComboBox", "AXSearchField": + return true + case "AXWebArea", "AXGroup", "AXScrollArea": + return isEditable + default: + return false + } + } + } /// Copy text to the system clipboard func copyToClipboard(_ text: String) { @@ -21,30 +40,7 @@ class ClipboardManager { /// Check if a text input field is currently focused func isTextFieldFocused() -> Bool { - // Use Accessibility API to check if focused element is a text field - guard let focusedApp = NSWorkspace.shared.frontmostApplication else { - return false - } - - let appElement = AXUIElementCreateApplication(focusedApp.processIdentifier) - - var focusedElement: CFTypeRef? - let result = AXUIElementCopyAttributeValue(appElement, kAXFocusedUIElementAttribute as CFString, &focusedElement) - - guard result == .success, let element = focusedElement else { - return false - } - - var role: CFTypeRef? - AXUIElementCopyAttributeValue(element as! AXUIElement, kAXRoleAttribute as CFString, &role) - - if let roleString = role as? String { - // Broaden supported roles to include web areas and editors (VS Code, Chrome, etc.) - let textRoles = ["AXTextField", "AXTextArea", "AXComboBox", "AXSearchField", "AXWebArea", "AXGroup", "AXStaticText"] - return textRoles.contains(roleString) - } - - return false + focusedElementState()?.isValidPasteTarget ?? false } /// Simulate Cmd+V to paste @@ -84,9 +80,71 @@ class ClipboardManager { NSApp.hide(nil) } - // Delay to allow focus to settle/switch back - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in - self?.simulatePaste() + // Wait for focus to return to a valid text target before posting Cmd+V. + attemptPasteWhenReady(remainingAttempts: autoPasteMaxAttempts) + } + + private func attemptPasteWhenReady(remainingAttempts: Int) { + guard remainingAttempts > 0 else { + let frontmostBundleIdentifier = + NSWorkspace.shared.frontmostApplication?.bundleIdentifier ?? "unknown" + let role = focusedElementState()?.role ?? "unknown" + print( + "⚠️ Auto-paste skipped: no editable target became ready, frontmostApp=\(frontmostBundleIdentifier), role=\(role)" + ) + return + } + + if let focusedElement = focusedElementState(), + focusedElement.bundleIdentifier != Bundle.main.bundleIdentifier, + focusedElement.isValidPasteTarget + { + print( + "✅ Auto-paste target ready: frontmostApp=\(focusedElement.bundleIdentifier), role=\(focusedElement.role ?? "unknown"), editable=\(focusedElement.isEditable)" + ) + simulatePaste() + return + } + + DispatchQueue.main.asyncAfter(deadline: .now() + autoPasteRetryDelay) { [weak self] in + self?.attemptPasteWhenReady(remainingAttempts: remainingAttempts - 1) } } + + private func focusedElementState() -> FocusedElementState? { + guard let focusedApp = NSWorkspace.shared.frontmostApplication else { + return nil + } + + let appElement = AXUIElementCreateApplication(focusedApp.processIdentifier) + + var focusedElement: CFTypeRef? + let result = AXUIElementCopyAttributeValue( + appElement, + kAXFocusedUIElementAttribute as CFString, + &focusedElement + ) + + guard result == .success, let element = focusedElement else { + return FocusedElementState( + bundleIdentifier: focusedApp.bundleIdentifier ?? "unknown", + role: nil, + isEditable: false + ) + } + + let axElement = unsafeBitCast(element, to: AXUIElement.self) + + var roleValue: CFTypeRef? + AXUIElementCopyAttributeValue(axElement, kAXRoleAttribute as CFString, &roleValue) + + var editableValue: CFTypeRef? + AXUIElementCopyAttributeValue(axElement, "AXEditable" as CFString, &editableValue) + + return FocusedElementState( + bundleIdentifier: focusedApp.bundleIdentifier ?? "unknown", + role: roleValue as? String, + isEditable: editableValue as? Bool ?? false + ) + } } From 93232dda40d506a3c3f86321cbd8b99bcc88657e Mon Sep 17 00:00:00 2001 From: Harshith Pasupuleti Date: Sun, 22 Mar 2026 23:13:56 +0530 Subject: [PATCH 2/2] Guard auto-paste retries against stale focus changes. Made-with: Cursor --- .../Features/Clipboard/ClipboardManager.swift | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/dhavnii/Features/Clipboard/ClipboardManager.swift b/dhavnii/Features/Clipboard/ClipboardManager.swift index 96ef47f..f408d58 100644 --- a/dhavnii/Features/Clipboard/ClipboardManager.swift +++ b/dhavnii/Features/Clipboard/ClipboardManager.swift @@ -13,6 +13,7 @@ import Carbon class ClipboardManager { private let autoPasteRetryDelay: TimeInterval = 0.12 private let autoPasteMaxAttempts = 10 + private var autoPasteToken = UUID() private struct FocusedElementState { let bundleIdentifier: String @@ -21,7 +22,7 @@ class ClipboardManager { var isValidPasteTarget: Bool { switch role { - case "AXTextField", "AXTextArea", "AXComboBox", "AXSearchField": + case "AXTextField", "AXTextArea", "AXComboBox", "AXSearchField", "AXStaticText": return true case "AXWebArea", "AXGroup", "AXScrollArea": return isEditable @@ -38,8 +39,8 @@ class ClipboardManager { pasteboard.setString(text, forType: .string) } - /// Check if a text input field is currently focused - func isTextFieldFocused() -> Bool { + /// Check if the focused element can safely receive an auto-paste event + func isValidPasteTargetFocused() -> Bool { focusedElementState()?.isValidPasteTarget ?? false } @@ -63,6 +64,9 @@ class ClipboardManager { // Always copy to clipboard (mandatory) copyToClipboard(text) + let token = UUID() + autoPasteToken = token + let hasAccessibilityPermission = AXIsProcessTrusted() let frontmostBundleIdentifier = NSWorkspace.shared.frontmostApplication?.bundleIdentifier ?? "unknown" @@ -81,10 +85,15 @@ class ClipboardManager { } // Wait for focus to return to a valid text target before posting Cmd+V. - attemptPasteWhenReady(remainingAttempts: autoPasteMaxAttempts) + attemptPasteWhenReady(remainingAttempts: autoPasteMaxAttempts, token: token) } - private func attemptPasteWhenReady(remainingAttempts: Int) { + private func attemptPasteWhenReady(remainingAttempts: Int, token: UUID) { + guard token == autoPasteToken else { + print("⚠️ Auto-paste cancelled: stale request") + return + } + guard remainingAttempts > 0 else { let frontmostBundleIdentifier = NSWorkspace.shared.frontmostApplication?.bundleIdentifier ?? "unknown" @@ -99,6 +108,10 @@ class ClipboardManager { focusedElement.bundleIdentifier != Bundle.main.bundleIdentifier, focusedElement.isValidPasteTarget { + guard token == autoPasteToken else { + print("⚠️ Auto-paste cancelled before paste: stale request") + return + } print( "✅ Auto-paste target ready: frontmostApp=\(focusedElement.bundleIdentifier), role=\(focusedElement.role ?? "unknown"), editable=\(focusedElement.isEditable)" ) @@ -107,7 +120,7 @@ class ClipboardManager { } DispatchQueue.main.asyncAfter(deadline: .now() + autoPasteRetryDelay) { [weak self] in - self?.attemptPasteWhenReady(remainingAttempts: remainingAttempts - 1) + self?.attemptPasteWhenReady(remainingAttempts: remainingAttempts - 1, token: token) } }