diff --git a/oAI/Services/AppleMailService.swift b/oAI/Services/AppleMailService.swift index af42420..9b57161 100644 --- a/oAI/Services/AppleMailService.swift +++ b/oAI/Services/AppleMailService.swift @@ -77,18 +77,27 @@ final class AppleMailService { } /// Triggers the real macOS "Confab wants to control Mail" Automation permission prompt if - /// the user hasn't been asked yet (a no-op if already granted or denied). Deliberately NOT - /// dispatched to the background `queue` used for AppleScript execution — the system consent - /// prompt needs to be raised from the main thread/run loop to actually display (same as this - /// codebase's existing blocking `NSAlert.runModal()` calls); calling it off-main appears to - /// silently fail (no dialog, no error, no state change — the exact symptom Rune hit live). - /// `AppleMailService` has no explicit actor annotation, so under this project's - /// `-default-isolation=MainActor` build setting this method already runs on the main actor. + /// the user hasn't been asked yet (a no-op if already granted or denied). + /// + /// First attempt just called this synchronously from an `async` method, reasoning that + /// `-default-isolation=MainActor` makes it run on the main actor anyway — it didn't work live + /// (button flashed, no dialog, no state change, confirmed still broken even after a + /// `tccutil reset`). Being "MainActor-isolated" inside Swift's structured-concurrency/Task + /// execution model is NOT the same as a classic synchronous AppKit call — this blocking, + /// modal-dialog-presenting legacy C API needs to run from a genuine `DispatchQueue.main.async` + /// dispatch (the same execution shape as a normal button target-action), not from inside a + /// suspended `Task` continuation frame, to correctly nest its own run loop for the prompt. @discardableResult func requestAccess() async -> Bool { - let status = Self.checkMailAutomationPermission(askUserIfNeeded: true) - Log.mail.info("requestAccess: AEDeterminePermissionToAutomateTarget(askUserIfNeeded: true) returned \(status)") - return status == 0 + Log.mail.info("requestAccess: called") + return await withCheckedContinuation { continuation in + DispatchQueue.main.async { + Log.mail.info("requestAccess: about to call AEDeterminePermissionToAutomateTarget(askUserIfNeeded: true)") + let status = Self.checkMailAutomationPermission(askUserIfNeeded: true) + Log.mail.info("requestAccess: AEDeterminePermissionToAutomateTarget(askUserIfNeeded: true) returned \(status)") + continuation.resume(returning: status == 0) + } + } } private static func checkMailAutomationPermission(askUserIfNeeded: Bool) -> OSStatus {