From 56d49d7854468093484691f7e500d2ec48a5bef9 Mon Sep 17 00:00:00 2001 From: Rune Olsen Date: Wed, 19 Aug 2026 14:20:30 +0200 Subject: [PATCH] Dispatch the Mail permission prompt via DispatchQueue.main.async, not implicit MainActor isolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous fix assumed being MainActor-isolated inside an async method was equivalent to a classic synchronous AppKit call — it wasn't; Rune confirmed live it still did nothing (no dialog, no state change, even after a clean tccutil reset). AEDeterminePermissionToAutomateTarget is a blocking, modal-dialog-presenting legacy API and needs to run from a genuine DispatchQueue.main.async dispatch to correctly nest its own run loop, not from inside a suspended Task continuation frame. Added extra Log.mail checkpoints (call received / about to call AE / result) so the next attempt has real data to diagnose from either way. --- oAI/Services/AppleMailService.swift | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) 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 {