import AppKit import Carbon // AppKit is unavailable to Catalyst targets. This native bundle uses only // public AppKit/Carbon APIs and communicates with the client through Foundation. @MainActor @objc(SybilMacQuickQuestionPlugin) final class MacQuickQuestionPlugin: NSObject, SybilQuickQuestionPanelBridging, NSWindowDelegate { private weak var delegate: (any SybilQuickQuestionPanelDelegate)? private var panel: QuickQuestionPanel? private var panelView: QuickQuestionPanelView? private var hotKey: EventHotKeyRef? private var eventHandler: EventHandlerRef? private var outsideClickMonitor: Any? private var hotKeyState = SybilQuickQuestionHotKeyState() private var isDismissing = false private var shortcutAvailable = false private var windowlessActivity: NSObjectProtocol? private var latestState = SybilQuickQuestionPanelState() private var previousApplication: NSRunningApplication? required override init() { super.init() } var isVisible: Bool { panel?.isVisible == true } func start(delegate: any SybilQuickQuestionPanelDelegate) -> Int32 { self.delegate = delegate if windowlessActivity == nil { // Keep the hotkey service alive with no windows. This option does // not prevent sleep/App Nap or intercept an explicit Quit command. windowlessActivity = ProcessInfo.processInfo.beginActivity( options: .automaticTerminationDisabled, reason: "Quick Question global shortcut" ) } guard eventHandler == nil else { return noErr } var eventTypes = [ EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)), EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyReleased)) ] let context = Unmanaged.passUnretained(self).toOpaque() let handlerStatus = InstallEventHandler( GetApplicationEventTarget(), { _, event, context in guard let event, let context else { return OSStatus(eventNotHandledErr) } var identifier = EventHotKeyID() let status = GetEventParameter( event, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, MemoryLayout.size, nil, &identifier ) guard status == noErr, identifier.signature == 0x53595151, identifier.id == 1 else { return OSStatus(eventNotHandledErr) } let isKeyDown = GetEventKind(event) == UInt32(kEventHotKeyPressed) let plugin = Unmanaged.fromOpaque(context).takeUnretainedValue() // Application event handlers run on the main event loop. MainActor.assumeIsolated { if plugin.hotKeyState.shouldToggle(isKeyDown: isKeyDown) { plugin.delegate?.quickQuestionToggleRequested() } } return noErr }, eventTypes.count, &eventTypes, context, &eventHandler ) guard handlerStatus == noErr else { return handlerStatus } let status = RegisterEventHotKey( UInt32(kVK_Space), UInt32(optionKey), EventHotKeyID(signature: 0x53595151, id: 1), GetApplicationEventTarget(), 0, &hotKey ) shortcutAvailable = status == noErr return status } func updateState(_ data: Data) { guard let state = try? JSONDecoder().decode(SybilQuickQuestionPanelState.self, from: data) else { return } // Updating a background request must not create a non-hosting window. // The controller creates/orders the panel only after UIKit is foreground. latestState = state panelView?.update(state, shortcutAvailable: shortcutAvailable) if isVisible { resizePanel() } } func show() { createPanelIfNeeded() guard let panel, let panelView else { return } panelView.update(latestState, shortcutAvailable: shortcutAvailable) let screen = NSScreen.screens.first { $0.frame.contains(NSEvent.mouseLocation) } ?? NSScreen.main let visibleFrame = screen?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1280, height: 800) let width = min(680, visibleFrame.width - 48) panel.setContentSize(NSSize(width: width, height: 240)) panelView.layoutSubtreeIfNeeded() let height = min(panelView.preferredHeight, visibleFrame.height - 64) let top = visibleFrame.maxY - min(visibleFrame.height * 0.18, 160) let y = max(visibleFrame.minY + 24, top - height) panel.setFrame(NSRect(x: visibleFrame.midX - width / 2, y: y, width: width, height: height), display: true) // The controller has verified UIKit foreground readiness. A // nonactivating panel can then take focus without raising a workspace. panel.makeKeyAndOrderFront(nil) panel.makeFirstResponder(panelView.promptView) panelView.preparePromptForPresentation() installOutsideClickMonitor() } func hide() { dismissPanel(restoringPreviousApplication: true) } func activateForQuickQuestion() { if let frontmost = NSWorkspace.shared.frontmostApplication, frontmost != NSRunningApplication.current { previousApplication = frontmost } // Do not order a window here. Catalyst must finish its own foreground // transition before the controller calls show(). NSApp.activate() } func activateMainWindow() { // An explicit Open in Chat/Open Sybil action keeps focus in Sybil. previousApplication = nil NSApp.activate() NSApp.windows.first { $0 !== panel && $0.canBecomeMain }?.makeKeyAndOrderFront(nil) } private func dismissPanel(restoringPreviousApplication: Bool) { guard !isDismissing else { return } let wasVisible = isVisible isDismissing = true if wasVisible { panel?.orderOut(nil) } removeOutsideClickMonitor() isDismissing = false let applicationToRestore = previousApplication previousApplication = nil if restoringPreviousApplication, NSApp.isActive, let applicationToRestore, !applicationToRestore.isTerminated { NSApp.yieldActivation(to: applicationToRestore) applicationToRestore.activate(options: []) } if wasVisible { delegate?.quickQuestionPanelDismissed() } } func stop() { previousApplication = nil hide() removeOutsideClickMonitor() if let hotKey { UnregisterEventHotKey(hotKey) } if let eventHandler { RemoveEventHandler(eventHandler) } hotKey = nil eventHandler = nil hotKeyState = SybilQuickQuestionHotKeyState() if let windowlessActivity { ProcessInfo.processInfo.endActivity(windowlessActivity) } windowlessActivity = nil delegate = nil } func windowDidResignKey(_ notification: Notification) { // Focus is already moving to a window/app the user chose. dismissPanel(restoringPreviousApplication: false) } private func createPanelIfNeeded() { guard panel == nil else { return } let panel = QuickQuestionPanel( contentRect: NSRect(x: 0, y: 0, width: 680, height: 240), styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false ) panel.title = "Quick Question" panel.level = .floating panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .transient, .ignoresCycle] panel.isFloatingPanel = true panel.hidesOnDeactivate = false panel.becomesKeyOnlyIfNeeded = false panel.worksWhenModal = true panel.isReleasedWhenClosed = false panel.isOpaque = false panel.backgroundColor = .clear panel.hasShadow = true panel.isMovableByWindowBackground = true panel.appearance = NSAppearance(named: .darkAqua) panel.delegate = self panel.onDismiss = { [weak self] in self?.hide() } let view = QuickQuestionPanelView() view.delegate = delegate view.onDismiss = { [weak self] in self?.hide() } panel.contentView = view panelView = view self.panel = panel } private func resizePanel() { guard let panel, let panelView else { return } panelView.layoutSubtreeIfNeeded() let visibleFrame = panel.screen?.visibleFrame ?? panel.frame let height = min(panelView.preferredHeight, visibleFrame.height - 64) guard abs(height - panel.frame.height) > 1 else { return } var frame = panel.frame frame.origin.y = max(visibleFrame.minY + 24, frame.maxY - height) frame.size.height = height panel.setFrame(frame, display: true) } private func installOutsideClickMonitor() { guard outsideClickMonitor == nil else { return } // Mouse-only monitoring does not require Accessibility or Input // Monitoring permission. No global keyboard event tap is installed. outsideClickMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self] _ in Task { @MainActor [weak self] in self?.hide() } } } private func removeOutsideClickMonitor() { if let outsideClickMonitor { NSEvent.removeMonitor(outsideClickMonitor) } outsideClickMonitor = nil } } @MainActor private final class QuickQuestionPanel: NSPanel { var onDismiss: (() -> Void)? override var canBecomeKey: Bool { true } override var canBecomeMain: Bool { false } override func cancelOperation(_ sender: Any?) { onDismiss?() } }