diff --git a/apps/macos/Sources/Clawdis/ControlRequestHandler.swift b/apps/macos/Sources/Clawdis/ControlRequestHandler.swift index 30a578c33..3c7057de6 100644 --- a/apps/macos/Sources/Clawdis/ControlRequestHandler.swift +++ b/apps/macos/Sources/Clawdis/ControlRequestHandler.swift @@ -14,9 +14,9 @@ enum ControlRequestHandler { } switch request { - case let .notify(title, body, sound): + case let .notify(title, body, sound, priority): let chosenSound = sound?.trimmingCharacters(in: .whitespacesAndNewlines) - let ok = await notifier.send(title: title, body: body, sound: chosenSound) + let ok = await notifier.send(title: title, body: body, sound: chosenSound, priority: priority) return ok ? Response(ok: true) : Response(ok: false, message: "notification not authorized") case let .ensurePermissions(caps, interactive): diff --git a/apps/macos/Sources/Clawdis/NotificationManager.swift b/apps/macos/Sources/Clawdis/NotificationManager.swift index f2d51bb9b..3f48da8c2 100644 --- a/apps/macos/Sources/Clawdis/NotificationManager.swift +++ b/apps/macos/Sources/Clawdis/NotificationManager.swift @@ -1,9 +1,10 @@ +import ClawdisIPC import Foundation import UserNotifications @MainActor struct NotificationManager { - func send(title: String, body: String, sound: String?) async -> Bool { + func send(title: String, body: String, sound: String?, priority: NotificationPriority? = nil) async -> Bool { let center = UNUserNotificationCenter.current() let status = await center.notificationSettings() if status.authorizationStatus == .notDetermined { @@ -20,6 +21,18 @@ struct NotificationManager { content.sound = UNNotificationSound(named: UNNotificationSoundName(soundName)) } + // Set interruption level based on priority + if let priority { + switch priority { + case .passive: + content.interruptionLevel = .passive + case .active: + content.interruptionLevel = .active + case .timeSensitive: + content.interruptionLevel = .timeSensitive + } + } + let req = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) do { try await center.add(req) diff --git a/apps/macos/Sources/ClawdisCLI/main.swift b/apps/macos/Sources/ClawdisCLI/main.swift index f031f2280..45a3bda78 100644 --- a/apps/macos/Sources/ClawdisCLI/main.swift +++ b/apps/macos/Sources/ClawdisCLI/main.swift @@ -54,17 +54,20 @@ struct ClawdisCLI { var title: String? var body: String? var sound: String? + var priority: NotificationPriority? while !args.isEmpty { let arg = args.removeFirst() switch arg { case "--title": title = args.popFirst() case "--body": body = args.popFirst() case "--sound": sound = args.popFirst() + case "--priority": + if let val = args.popFirst(), let p = NotificationPriority(rawValue: val) { priority = p } default: break } } guard let t = title, let b = body else { throw CLIError.help } - return .notify(title: t, body: b, sound: sound) + return .notify(title: t, body: b, sound: sound, priority: priority) case "ensure-permissions": var caps: [Capability] = [] @@ -169,7 +172,7 @@ struct ClawdisCLI { clawdis-mac — talk to the running Clawdis.app XPC service Usage: - clawdis-mac notify --title --body [--sound ] + clawdis-mac notify --title --body [--sound ] [--priority ] clawdis-mac ensure-permissions [--cap ] [--interactive] diff --git a/apps/macos/Sources/ClawdisIPC/IPC.swift b/apps/macos/Sources/ClawdisIPC/IPC.swift index 9bdfe8a47..8eacc033f 100644 --- a/apps/macos/Sources/ClawdisIPC/IPC.swift +++ b/apps/macos/Sources/ClawdisIPC/IPC.swift @@ -14,8 +14,15 @@ public enum Capability: String, Codable, CaseIterable, Sendable { // MARK: - Requests +/// Notification interruption level (maps to UNNotificationInterruptionLevel) +public enum NotificationPriority: String, Codable, Sendable { + case passive // silent, no wake + case active // default + case timeSensitive // breaks through Focus modes +} + public enum Request: Sendable { - case notify(title: String, body: String, sound: String?) + case notify(title: String, body: String, sound: String?, priority: NotificationPriority?) case ensurePermissions([Capability], interactive: Bool) case screenshot(displayID: UInt32?, windowID: UInt32?, format: String) case runShell( @@ -49,7 +56,7 @@ public struct Response: Codable, Sendable { extension Request: Codable { private enum CodingKeys: String, CodingKey { case type - case title, body, sound + case title, body, sound, priority case caps, interactive case displayID, windowID, format case command, cwd, env, timeoutSec, needsScreenRecording @@ -70,11 +77,12 @@ extension Request: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { - case let .notify(title, body, sound): + case let .notify(title, body, sound, priority): try container.encode(Kind.notify, forKey: .type) try container.encode(title, forKey: .title) try container.encode(body, forKey: .body) try container.encodeIfPresent(sound, forKey: .sound) + try container.encodeIfPresent(priority, forKey: .priority) case let .ensurePermissions(caps, interactive): try container.encode(Kind.ensurePermissions, forKey: .type) @@ -119,7 +127,8 @@ extension Request: Codable { let title = try container.decode(String.self, forKey: .title) let body = try container.decode(String.self, forKey: .body) let sound = try container.decodeIfPresent(String.self, forKey: .sound) - self = .notify(title: title, body: body, sound: sound) + let priority = try container.decodeIfPresent(NotificationPriority.self, forKey: .priority) + self = .notify(title: title, body: body, sound: sound, priority: priority) case .ensurePermissions: let caps = try container.decode([Capability].self, forKey: .caps)