diff --git a/apps/macos/LauncherApp/look-app/Support/Actions/ActionController+Links.swift b/apps/macos/LauncherApp/look-app/Support/Actions/ActionController+Links.swift index 5842a640..a7b3dc78 100644 --- a/apps/macos/LauncherApp/look-app/Support/Actions/ActionController+Links.swift +++ b/apps/macos/LauncherApp/look-app/Support/Actions/ActionController+Links.swift @@ -21,7 +21,7 @@ extension ActionController { // retrying on that loops. guard !didAsk else { return Self.noCalendarAccess } Task { - await EventKitService.shared.requestCalendarAccess() + await PermissionPrompt.run { await EventKitService.shared.requestCalendarAccess() } setFeedback(presentJoinChoices(named: name, didAsk: true)) } return "" @@ -68,7 +68,7 @@ extension ActionController { // Once only: see the calendar branch. guard !didAsk else { return Self.noContactsAccess } Task { - await ContactsService.shared.requestAccess() + await PermissionPrompt.run { await ContactsService.shared.requestAccess() } setFeedback(presentCallChoices(named: name, modality: modality, didAsk: true)) } return "" diff --git a/apps/macos/LauncherApp/look-app/Support/Launcher/PermissionPrompt.swift b/apps/macos/LauncherApp/look-app/Support/Launcher/PermissionPrompt.swift new file mode 100644 index 00000000..1918ab5d --- /dev/null +++ b/apps/macos/LauncherApp/look-app/Support/Launcher/PermissionPrompt.swift @@ -0,0 +1,18 @@ +import AppKit + +/// Runs a TCC request with the launcher pinned open. +/// +/// The system dialog takes focus, which fires `didResignActive` and hides the +/// window. The app is then in the background, where macOS will not present the +/// next prompt, so a sequence of requests dies after the first one. +@MainActor +enum PermissionPrompt { + private(set) static var isPresenting = false + + static func run(_ request: () async -> Void) async { + isPresenting = true + NSApplication.shared.activate(ignoringOtherApps: true) + await request() + isPresenting = false + } +} diff --git a/apps/macos/LauncherApp/look-app/Views/Launcher/LauncherView.swift b/apps/macos/LauncherApp/look-app/Views/Launcher/LauncherView.swift index 0d755fd6..9e455b49 100644 --- a/apps/macos/LauncherApp/look-app/Views/Launcher/LauncherView.swift +++ b/apps/macos/LauncherApp/look-app/Views/Launcher/LauncherView.swift @@ -929,7 +929,9 @@ struct LauncherView: View { .onReceive( NotificationCenter.default.publisher(for: NSApplication.didResignActiveNotification) ) { _ in - if launcherWindow()?.isVisible == true { + // Not while a TCC dialog is up: it steals focus, and hiding here + // backgrounds the app so the next prompt never appears. + if !PermissionPrompt.isPresenting, launcherWindow()?.isVisible == true { Logger(subsystem: "noah-code.Look", category: "window-resize") .debug("didResignActiveNotification -> hideLauncherWindow(restorePreviousApp: false)") hideLauncherWindow(restorePreviousApp: false) diff --git a/apps/macos/LauncherApp/look-app/Views/Settings/PermissionsRow.swift b/apps/macos/LauncherApp/look-app/Views/Settings/PermissionsRow.swift index f3e23e4f..b7a195b5 100644 --- a/apps/macos/LauncherApp/look-app/Views/Settings/PermissionsRow.swift +++ b/apps/macos/LauncherApp/look-app/Views/Settings/PermissionsRow.swift @@ -157,10 +157,12 @@ struct PermissionsRow: View { } private func request(_ capability: PermissionItem.Capability) async { - switch capability { - case .calendar: await EventKitService.shared.requestCalendarAccess() - case .reminders: await EventKitService.shared.requestReminderAccess() - case .contacts: await ContactsService.shared.requestAccess() + await PermissionPrompt.run { + switch capability { + case .calendar: await EventKitService.shared.requestCalendarAccess() + case .reminders: await EventKitService.shared.requestReminderAccess() + case .contacts: await ContactsService.shared.requestAccess() + } } }