import AppKit import OpenClawKit import OSLog final class PairingAlertHostWindow: NSWindow { override var canBecomeKey: Bool { true } override var canBecomeMain: Bool { true } } @MainActor final class PairingAlertState { var activeAlert: NSAlert? var activeRequestId: String? var alertHostWindow: NSWindow? } @MainActor enum PairingAlertSupport { enum PairingResolution: String { case approved case rejected } struct PairingResolvedEvent: Codable { let requestId: String let decision: String let ts: Double } static func endActiveAlert(activeAlert: inout NSAlert?, activeRequestId: inout String?) { guard let alert = activeAlert else { return } if let parent = alert.window.sheetParent { parent.endSheet(alert.window, returnCode: .abort) } activeAlert = nil activeRequestId = nil } static func endActiveAlert(state: PairingAlertState) { self.endActiveAlert(activeAlert: &state.activeAlert, activeRequestId: &state.activeRequestId) } static func requireAlertHostWindow(alertHostWindow: inout NSWindow?) -> NSWindow { if let alertHostWindow { return alertHostWindow } let window = PairingAlertHostWindow( contentRect: NSRect(x: 0, y: 0, width: 520, height: 1), styleMask: [.borderless], backing: .buffered, defer: false) window.title = "" window.isReleasedWhenClosed = false window.level = .floating window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] window.isOpaque = false window.hasShadow = false window.backgroundColor = .clear window.ignoresMouseEvents = true alertHostWindow = window return window } static func configureDefaultPairingAlert( _ alert: NSAlert, messageText: String, informativeText: String) { alert.alertStyle = .warning alert.messageText = messageText alert.informativeText = informativeText alert.addButton(withTitle: "Later") alert.addButton(withTitle: "Approve") alert.addButton(withTitle: "Reject") if #available(macOS 11.0, *), alert.buttons.indices.contains(2) { alert.buttons[2].hasDestructiveAction = true } } static func beginCenteredSheet( alert: NSAlert, hostWindow: NSWindow, completionHandler: @escaping (NSApplication.ModalResponse) -> Void) { let sheetSize = alert.window.frame.size if let screen = hostWindow.screen ?? NSScreen.main { let bounds = screen.visibleFrame let x = bounds.midX - (sheetSize.width / 2) let sheetOriginY = bounds.midY - (sheetSize.height / 2) let hostY = sheetOriginY + sheetSize.height - hostWindow.frame.height hostWindow.setFrameOrigin(NSPoint(x: x, y: hostY)) } else { hostWindow.center() } hostWindow.makeKeyAndOrderFront(nil) alert.beginSheetModal(for: hostWindow, completionHandler: completionHandler) } static func runPairingPushTask( bufferingNewest: Int = 200, loadPending: @escaping @MainActor () async -> Void, handlePush: @escaping @MainActor (GatewayPush) -> Void) async { _ = try? await GatewayConnection.shared.refresh() await loadPending() await GatewayPushSubscription.consume(bufferingNewest: bufferingNewest, onPush: handlePush) } static func startPairingPushTask( task: inout Task?, isStopping: inout Bool, bufferingNewest: Int = 200, loadPending: @escaping @MainActor () async -> Void, handlePush: @escaping @MainActor (GatewayPush) -> Void) { guard task == nil else { return } isStopping = false task = Task { await self.runPairingPushTask( bufferingNewest: bufferingNewest, loadPending: loadPending, handlePush: handlePush) } } static func beginPairingAlert( messageText: String, informativeText: String, alertHostWindow: inout NSWindow?, completion: @escaping (NSApplication.ModalResponse, NSWindow) -> Void) -> NSAlert { NSApp.activate(ignoringOtherApps: true) let alert = NSAlert() self.configureDefaultPairingAlert(alert, messageText: messageText, informativeText: informativeText) let hostWindow = self.requireAlertHostWindow(alertHostWindow: &alertHostWindow) self.beginCenteredSheet(alert: alert, hostWindow: hostWindow) { response in completion(response, hostWindow) } return alert } static func presentPairingAlert( requestId: String, messageText: String, informativeText: String, activeAlert: inout NSAlert?, activeRequestId: inout String?, alertHostWindow: inout NSWindow?, completion: @escaping (NSApplication.ModalResponse, NSWindow) -> Void) { activeRequestId = requestId activeAlert = self.beginPairingAlert( messageText: messageText, informativeText: informativeText, alertHostWindow: &alertHostWindow, completion: completion) } static func presentPairingAlert( request: Request, requestId: String, messageText: String, informativeText: String, state: PairingAlertState, onResponse: @escaping @MainActor (NSApplication.ModalResponse, Request) async -> Void) { self.presentPairingAlert( requestId: requestId, messageText: messageText, informativeText: informativeText, activeAlert: &state.activeAlert, activeRequestId: &state.activeRequestId, alertHostWindow: &state.alertHostWindow, completion: { response, hostWindow in Task { @MainActor in self.clearActivePairingAlert(state: state, hostWindow: hostWindow) await onResponse(response, request) } }) } static func clearActivePairingAlert( activeAlert: inout NSAlert?, activeRequestId: inout String?, hostWindow: NSWindow) { activeRequestId = nil activeAlert = nil hostWindow.orderOut(nil) } static func clearActivePairingAlert(state: PairingAlertState, hostWindow: NSWindow) { self.clearActivePairingAlert( activeAlert: &state.activeAlert, activeRequestId: &state.activeRequestId, hostWindow: hostWindow) } static func stopPairingPrompter( isStopping: inout Bool, activeAlert: inout NSAlert?, activeRequestId: inout String?, task: inout Task?, queue: inout [some Any], isPresenting: inout Bool, alertHostWindow: inout NSWindow?) { isStopping = true self.endActiveAlert(activeAlert: &activeAlert, activeRequestId: &activeRequestId) task?.cancel() task = nil queue.removeAll(keepingCapacity: false) isPresenting = false activeRequestId = nil alertHostWindow?.orderOut(nil) alertHostWindow?.close() alertHostWindow = nil } static func stopPairingPrompter( isStopping: inout Bool, task: inout Task?, queue: inout [some Any], isPresenting: inout Bool, state: PairingAlertState) { self.stopPairingPrompter( isStopping: &isStopping, activeAlert: &state.activeAlert, activeRequestId: &state.activeRequestId, task: &task, queue: &queue, isPresenting: &isPresenting, alertHostWindow: &state.alertHostWindow) } static func approveRequest( requestId: String, kind: String, logger: Logger, action: @escaping () async throws -> Void) async -> Bool { do { try await action() logger.info("approved \(kind, privacy: .public) pairing requestId=\(requestId, privacy: .public)") return true } catch { logger.error("approve failed requestId=\(requestId, privacy: .public)") logger.error("approve failed: \(error.localizedDescription, privacy: .public)") return false } } static func rejectRequest( requestId: String, kind: String, logger: Logger, action: @escaping () async throws -> Void) async { do { try await action() logger.info("rejected \(kind, privacy: .public) pairing requestId=\(requestId, privacy: .public)") } catch { logger.error("reject failed requestId=\(requestId, privacy: .public)") logger.error("reject failed: \(error.localizedDescription, privacy: .public)") } } }