Dispatch the Mail permission prompt via DispatchQueue.main.async, not implicit MainActor isolation

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.
This commit is contained in:
2026-08-19 14:20:30 +02:00
parent da7bce26a2
commit 56d49d7854
+17 -8
View File
@@ -77,18 +77,27 @@ final class AppleMailService {
} }
/// Triggers the real macOS "Confab wants to control Mail" Automation permission prompt if /// 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 /// the user hasn't been asked yet (a no-op if already granted or denied).
/// 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 /// First attempt just called this synchronously from an `async` method, reasoning that
/// codebase's existing blocking `NSAlert.runModal()` calls); calling it off-main appears to /// `-default-isolation=MainActor` makes it run on the main actor anyway it didn't work live
/// silently fail (no dialog, no error, no state change the exact symptom Rune hit live). /// (button flashed, no dialog, no state change, confirmed still broken even after a
/// `AppleMailService` has no explicit actor annotation, so under this project's /// `tccutil reset`). Being "MainActor-isolated" inside Swift's structured-concurrency/Task
/// `-default-isolation=MainActor` build setting this method already runs on the main actor. /// 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 @discardableResult
func requestAccess() async -> Bool { func requestAccess() async -> Bool {
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) let status = Self.checkMailAutomationPermission(askUserIfNeeded: true)
Log.mail.info("requestAccess: AEDeterminePermissionToAutomateTarget(askUserIfNeeded: true) returned \(status)") Log.mail.info("requestAccess: AEDeterminePermissionToAutomateTarget(askUserIfNeeded: true) returned \(status)")
return status == 0 continuation.resume(returning: status == 0)
}
}
} }
private static func checkMailAutomationPermission(askUserIfNeeded: Bool) -> OSStatus { private static func checkMailAutomationPermission(askUserIfNeeded: Bool) -> OSStatus {