import AppKit import Carbon import Foundation import WebKit import UserNotifications struct Manifest: Decodable { let baseUrl: String let statusUrl: String let openAgentUrl: String let profiles: [Profile] let hotkeys: [Hotkey] } struct Profile: Decodable { let name: String let toggleShortcut: String } struct Hotkey: Decodable { let shortcut: String let endpoint: String } enum RecordingState: Int, Decodable { case idle = 0 case recording = 1 case summarizing = 2 } struct RecordingStatus: Decodable { let isRecording: Bool let state: RecordingState let launchProfile: String? } @main @MainActor final class DesktopControls: NSObject, NSApplicationDelegate, NSMenuDelegate, UNUserNotificationCenterDelegate { private let manifest: Manifest private var status = RecordingStatus(isRecording: false, state: .idle, launchProfile: nil) private var statusItem: NSStatusItem! private var registeredHotkeys: [UInt32: Hotkey] = [:] private var timer: Timer? private var agentWindow: NSWindow? private var independentAgentWindows: [String: NSWindow] = [:] private var agentWindowState = DesktopAgentWindowState() private var requests = DesktopUserRequestState() private var fetchingRequests = false private var fetchingWindows = false private var authorizationInFlight = false private var authorizationChecked = false private var notificationsAuthorized = false private lazy var notificationCenter = UNUserNotificationCenter.current() override init() { guard CommandLine.arguments.count == 2, let data = try? Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[1])), let decoded = try? JSONDecoder().decode(Manifest.self, from: data) else { fputs("Usage: macos-desktop-controls \n", stderr) exit(2) } manifest = decoded super.init() } static func main() { if CommandLine.arguments.count == 2 && CommandLine.arguments[1] == "--self-test" { runDesktopControlSelfTests() return } // Notification activation after shutdown has no trusted service manifest. if CommandLine.arguments.count == 1 { fputs("Ignoring desktop notification activation without an active service manifest.\n", stderr) return } guard Bundle.main.bundleIdentifier == "cloud.schweigert.meeting-assistant.desktop-controls" else { fputs("Desktop controls must run from the signed MeetingAssistantDesktopControls.app bundle.\n", stderr) exit(2) } let application = NSApplication.shared let delegate = DesktopControls() application.delegate = delegate application.setActivationPolicy(.accessory) application.run() } func applicationWillFinishLaunching(_ notification: Notification) { notificationCenter.delegate = self notificationCenter.removeAllPendingNotificationRequests() notificationCenter.removeAllDeliveredNotifications() } func applicationDidFinishLaunching(_ notification: Notification) { statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) statusItem.button?.image = NSImage( systemSymbolName: "waveform.circle", accessibilityDescription: "Meeting Assistant") let menu = NSMenu() menu.delegate = self statusItem.menu = menu registerHotkeys() refresh() timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in DispatchQueue.main.async { self?.refresh() } } } func applicationWillTerminate(_ notification: Notification) { timer?.invalidate() notificationCenter.removeAllPendingNotificationRequests() notificationCenter.removeAllDeliveredNotifications() } func menuWillOpen(_ menu: NSMenu) { removeNotifications(requests.reconcile(requests.pending, now: Date())) rebuildMenu(menu) refresh() } private func rebuildMenu(_ menu: NSMenu) { menu.removeAllItems() let stateItem = NSMenuItem(title: tooltip, action: nil, keyEquivalent: "") stateItem.isEnabled = false menu.addItem(stateItem) menu.addItem(.separator()) menu.addItem(item("Open agent", #selector(openAgent))) if status.isRecording { menu.addItem(item("Stop meeting recording and transcribe", #selector(stopRecording))) menu.addItem(item("Cancel meeting recording and discard", #selector(abortRecording))) } else { for (index, profile) in manifest.profiles.enumerated() { let entry = item( "Start meeting recording (\(profile.name))", #selector(startProfile(_:))) entry.tag = index entry.keyEquivalent = menuKey(from: profile.toggleShortcut) entry.keyEquivalentModifierMask = menuModifiers(from: profile.toggleShortcut) menu.addItem(entry) } } menu.addItem(.separator()) let pending = NSMenuItem(title: "Pending requests (\(requests.pending.count))", action: nil, keyEquivalent: "") let pendingMenu = NSMenu() if requests.pending.isEmpty { let empty = NSMenuItem(title: "No pending requests", action: nil, keyEquivalent: "") empty.isEnabled = false pendingMenu.addItem(empty) } for request in requests.pending { let entry = NSMenuItem(title: request.title, action: nil, keyEquivalent: "") entry.toolTip = request.message let decisions = NSMenu() let message = NSMenuItem(title: request.message, action: nil, keyEquivalent: "") message.isEnabled = false decisions.addItem(message) decisions.addItem(.separator()) for action in request.actions { let decision = item(action.label, #selector(answerRequest(_:))) decision.representedObject = DesktopRequestDecision(request.id, action.id) decision.isEnabled = !requests.isResponding(request.id) decisions.addItem(decision) } entry.submenu = decisions pendingMenu.addItem(entry) } pending.submenu = pendingMenu menu.addItem(pending) menu.addItem(.separator()) menu.addItem(item("Exit", #selector(exitApplication))) } private var tooltip: String { let suffix = requests.pending.isEmpty ? "" : " — \(requests.pending.count) pending request(s)" if status.isRecording { return (status.launchProfile.map { "Meeting Assistant — recording (\($0))" } ?? "Meeting Assistant — recording") + suffix } return (status.state == .summarizing ? "Meeting Assistant — summarizing" : "Meeting Assistant — idle") + suffix } private func item(_ title: String, _ action: Selector) -> NSMenuItem { let result = NSMenuItem(title: title, action: action, keyEquivalent: "") result.target = self return result } @objc private func openAgent() { if agentWindow == nil, let url = URL(string: manifest.openAgentUrl) { agentWindow = makeAgentWindow(url) } NSApplication.shared.activate(ignoringOtherApps: true) agentWindow?.makeKeyAndOrderFront(nil) } private func makeAgentWindow(_ url: URL) -> NSWindow { let webView = WKWebView(frame: .zero) let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 840, height: 760), styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false) window.title = "Meeting Summary Agent" window.contentView = webView window.center() window.isReleasedWhenClosed = false webView.load(URLRequest(url: url)) return window } @objc private func answerRequest(_ sender: NSMenuItem) { guard let decision = sender.representedObject as? DesktopRequestDecision else { return } submitResponse(requestId: decision.requestId, action: decision.action) } private func submitResponse(requestId: String, action: String) { guard let response = requests.beginResponse(baseUrl: manifest.baseUrl, requestId: requestId, action: action, now: Date()) else { return } URLSession.shared.dataTask(with: response) { [weak self] _, response, error in let accepted = error == nil && (response as? HTTPURLResponse).map { (200..<300).contains($0.statusCode) } == true DispatchQueue.main.async { guard let self else { return } self.requests.completeResponse(requestId, accepted: accepted) if accepted { self.removeNotifications([requestId]) } if let error { desktopLog("User response failed: \(error.localizedDescription)") } self.refresh() } }.resume() } nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.banner, .list, .sound]) } nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { // Closing the announcement is independent of deciding its menu request. guard response.actionIdentifier != UNNotificationDismissActionIdentifier, response.actionIdentifier != UNNotificationDefaultActionIdentifier, let id = response.notification.request.content.userInfo["requestId"] as? String else { completionHandler() return } let action = response.actionIdentifier DispatchQueue.main.async { [weak self] in self?.submitResponse(requestId: id, action: action) } completionHandler() } @objc private func startProfile(_ sender: NSMenuItem) { let profile = manifest.profiles[sender.tag] post(path: "/profiles/\(escaped(profile.name))/recording/start") } @objc private func stopRecording() { post(path: "/recording/stop") } @objc private func abortRecording() { post(path: "/recording/abort") } @objc private func exitApplication() { post(path: "/application/exit") DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { NSApplication.shared.terminate(nil) } } private func refresh() { removeNotifications(requests.reconcile(requests.pending, now: Date())) updateStatusItem() refreshStatus() refreshRequests() refreshAgentWindows() } private func updateStatusItem() { statusItem.button?.image = NSImage( systemSymbolName: status.isRecording ? "record.circle.fill" : "waveform.circle", accessibilityDescription: tooltip) statusItem.button?.toolTip = tooltip } private func refreshRequests() { guard !fetchingRequests, let url = URL(string: manifest.baseUrl + "/desktop/requests") else { return } fetchingRequests = true URLSession.shared.dataTask(with: url) { [weak self] data, response, error in var snapshot: [DesktopUserRequest]? if let data, error == nil, (response as? HTTPURLResponse)?.statusCode == 200 { do { snapshot = try decodeDesktopRequests(data) } catch { desktopLog("Cannot decode pending desktop requests: \(error)") } } DispatchQueue.main.async { guard let self else { return } self.fetchingRequests = false guard let snapshot else { return } self.removeNotifications(self.requests.reconcile(snapshot, now: Date())) self.updateStatusItem() self.announcePendingRequests() } }.resume() } private func removeNotifications(_ ids: [String]) { guard !ids.isEmpty else { return } let identifiers = ids.map { "desktop-request." + $0 } notificationCenter.removePendingNotificationRequests(withIdentifiers: identifiers) notificationCenter.removeDeliveredNotifications(withIdentifiers: identifiers) } private func announcePendingRequests() { guard !requests.notificationsToAnnounce().isEmpty else { return } if !authorizationChecked { guard !authorizationInFlight else { return } authorizationInFlight = true notificationCenter.getNotificationSettings { [weak self] settings in DispatchQueue.main.async { guard let self else { return } if settings.authorizationStatus == .notDetermined { self.notificationCenter.requestAuthorization(options: [.alert, .sound]) { [weak self] granted, error in DispatchQueue.main.async { if let error { desktopLog("Notification authorization failed: \(error.localizedDescription)") } self?.finishNotificationAuthorization(granted) } } } else { self.finishNotificationAuthorization(settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional) } } } return } guard notificationsAuthorized else { return } notificationCenter.setNotificationCategories(Set(requests.pending.map { $0.notificationCategory() })) for request in requests.notificationsToAnnounce() { requests.markAnnounced(request.id) notificationCenter.add(request.notificationRequest()) { error in if let error { desktopLog("Cannot schedule desktop notification \(request.id): \(error.localizedDescription)") } else { desktopLog("Scheduled desktop notification \(request.id)") } } } } private func finishNotificationAuthorization(_ granted: Bool) { authorizationChecked = true authorizationInFlight = false notificationsAuthorized = granted desktopLog(granted ? "Desktop notifications authorized." : "Desktop notifications unavailable; pending requests remain in the menu.") removeNotifications(requests.reconcile(requests.pending, now: Date())) announcePendingRequests() } private func refreshAgentWindows() { guard !fetchingWindows, let url = URL(string: manifest.baseUrl + "/desktop/agent-windows") else { return } fetchingWindows = true URLSession.shared.dataTask(with: url) { [weak self] data, response, error in let windows = data.flatMap { data in error == nil && (response as? HTTPURLResponse)?.statusCode == 200 ? try? JSONDecoder().decode([DesktopAgentWindow].self, from: data) : nil } DispatchQueue.main.async { guard let self else { return } self.fetchingWindows = false guard let windows else { return } for requested in self.agentWindowState.takeUnopened(windows) { guard let url = URL(string: requested.url) else { continue } let window = self.makeAgentWindow(url) self.independentAgentWindows[requested.id] = window NSApplication.shared.activate(ignoringOtherApps: true) window.makeKeyAndOrderFront(nil) desktopLog("Opened independent workflow agent window \(requested.id)") } } }.resume() } private func refreshStatus() { guard let url = URL(string: manifest.statusUrl) else { return } URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in guard let data, let next = try? JSONDecoder().decode(RecordingStatus.self, from: data) else { return } DispatchQueue.main.async { self?.status = next self?.updateStatusItem() } }.resume() } private func post(path: String) { post(url: manifest.baseUrl + path) } private func post(url value: String) { guard let url = URL(string: value) else { return } var request = URLRequest(url: url) request.httpMethod = "POST" URLSession.shared.dataTask(with: request) { [weak self] _, _, _ in DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { self?.refresh() } }.resume() } private func registerHotkeys() { var eventType = EventTypeSpec( eventClass: OSType(kEventClassKeyboard), eventKind: OSType(kEventHotKeyPressed)) InstallEventHandler( GetApplicationEventTarget(), { _, event, context in guard let event, let context else { return noErr } var identifier = EventHotKeyID() GetEventParameter( event, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, MemoryLayout.size, nil, &identifier) let owner = Unmanaged.fromOpaque(context).takeUnretainedValue() if let hotkey = owner.registeredHotkeys[identifier.id] { owner.post(path: hotkey.endpoint) } return noErr }, 1, &eventType, Unmanaged.passUnretained(self).toOpaque(), nil) for (index, hotkey) in manifest.hotkeys.enumerated() { guard let keyCode = carbonKeyCode(from: hotkey.shortcut) else { continue } let identifier = EventHotKeyID(signature: fourCC("MTGA"), id: UInt32(index + 1)) var reference: EventHotKeyRef? let result = RegisterEventHotKey( keyCode, carbonModifiers(from: hotkey.shortcut), identifier, GetApplicationEventTarget(), 0, &reference) if result == noErr { registeredHotkeys[identifier.id] = hotkey } } } private func escaped(_ value: String) -> String { value.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? value } } private final class DesktopRequestDecision: NSObject { let requestId: String let action: String init(_ requestId: String, _ action: String) { self.requestId = requestId self.action = action } } private func desktopLog(_ message: String) { fputs("\(message)\n", stderr) } private func tokens(_ shortcut: String) -> [String] { shortcut.uppercased().split(separator: "+").map(String.init) } private func carbonModifiers(from shortcut: String) -> UInt32 { let parts = tokens(shortcut) var result: UInt32 = 0 if parts.contains("CTRL") || parts.contains("CONTROL") { result |= UInt32(controlKey) } if parts.contains("ALT") || parts.contains("OPTION") { result |= UInt32(optionKey) } if parts.contains("SHIFT") { result |= UInt32(shiftKey) } if parts.contains("WIN") || parts.contains("WINDOWS") || parts.contains("CMD") || parts.contains("COMMAND") { result |= UInt32(cmdKey) } return result } private func menuModifiers(from shortcut: String) -> NSEvent.ModifierFlags { let parts = tokens(shortcut) var result: NSEvent.ModifierFlags = [] if parts.contains("CTRL") || parts.contains("CONTROL") { result.insert(.control) } if parts.contains("ALT") || parts.contains("OPTION") { result.insert(.option) } if parts.contains("SHIFT") { result.insert(.shift) } if parts.contains("WIN") || parts.contains("WINDOWS") || parts.contains("CMD") || parts.contains("COMMAND") { result.insert(.command) } return result } private func menuKey(from shortcut: String) -> String { tokens(shortcut).last?.lowercased() ?? "" } private func carbonKeyCode(from shortcut: String) -> UInt32? { guard let key = tokens(shortcut).last, key.count == 1, let scalar = key.unicodeScalars.first else { return nil } let codes: [Character: UInt32] = [ "A": 0, "S": 1, "D": 2, "F": 3, "H": 4, "G": 5, "Z": 6, "X": 7, "C": 8, "V": 9, "B": 11, "Q": 12, "W": 13, "E": 14, "R": 15, "Y": 16, "T": 17, "M": 46, "L": 37, "N": 45, "P": 35, "O": 31, "I": 34, "U": 32, "J": 38, "K": 40 ] return codes[Character(String(scalar))] } private func fourCC(_ value: String) -> OSType { value.utf8.reduce(0) { ($0 << 8) + OSType($1) } }