fix: preserve custom commands for focused notifications

This commit is contained in:
Lawrence Chen 2026-03-20 01:02:42 -07:00 committed by Lawrence Chen
parent 60b6d8badc
commit c44e975855
2 changed files with 103 additions and 7 deletions

View file

@ -1046,15 +1046,19 @@ final class TerminalNotificationStore: ObservableObject {
center.removePendingNotificationRequestsOffMain(withIdentifiers: idsToClear)
}
private func resolvedNotificationTitle(for notification: TerminalNotification) -> String {
let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
?? "cmux"
return notification.title.isEmpty ? appName : notification.title
}
private func scheduleUserNotification(_ notification: TerminalNotification) {
ensureAuthorization(origin: .notificationDelivery) { [weak self] authorized in
guard let self, authorized else { return }
let content = UNMutableNotificationContent()
let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
?? "cmux"
content.title = notification.title.isEmpty ? appName : notification.title
content.title = self.resolvedNotificationTitle(for: notification)
content.subtitle = notification.subtitle
content.body = notification.body
content.sound = NotificationSoundSettings.sound()
@ -1088,8 +1092,12 @@ final class TerminalNotificationStore: ObservableObject {
}
private func playSuppressedNotificationFeedback(for notification: TerminalNotification) {
_ = notification
NotificationSoundSettings.playSelectedSound()
NotificationSoundSettings.runCustomCommand(
title: resolvedNotificationTitle(for: notification),
subtitle: notification.subtitle,
body: notification.body
)
}
private func ensureAuthorization(

View file

@ -401,8 +401,11 @@ final class NotificationDockBadgeTests: XCTestCase {
}
}
func testFocusedTerminalNotificationStillRunsLocalSoundFeedbackWhenExternalDeliveryIsSuppressed() {
let appDelegate = AppDelegate.shared ?? AppDelegate()
func testFocusedTerminalNotificationStillRunsLocalSoundFeedbackWhenExternalDeliveryIsSuppressed() throws {
guard let appDelegate = AppDelegate.shared else {
XCTFail("AppDelegate.shared must be set for this test")
return
}
let manager = TabManager()
let store = TerminalNotificationStore.shared
@ -445,9 +448,94 @@ final class NotificationDockBadgeTests: XCTestCase {
body: ""
)
let createdNotificationID = try XCTUnwrap(store.notifications.first?.id)
XCTAssertTrue(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: terminalPanel.id))
XCTAssertTrue(deliveredNotificationIDs.isEmpty)
XCTAssertEqual(localFeedbackNotificationIDs.count, 1)
XCTAssertEqual(localFeedbackNotificationIDs, [createdNotificationID])
}
func testFocusedTerminalSuppressedNotificationRunsCustomCommand() throws {
guard let appDelegate = AppDelegate.shared else {
XCTFail("AppDelegate.shared must be set for this test")
return
}
let manager = TabManager()
let store = TerminalNotificationStore.shared
let defaults = UserDefaults.standard
let commandOutputURL = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-notification-command-\(UUID().uuidString).txt", isDirectory: false)
let originalTabManager = appDelegate.tabManager
let originalNotificationStore = appDelegate.notificationStore
let originalAppFocusOverride = AppFocusState.overrideIsFocused
let hadSoundValue = defaults.object(forKey: NotificationSoundSettings.key) != nil
let originalSoundValue = defaults.object(forKey: NotificationSoundSettings.key)
let hadCommandValue = defaults.object(forKey: NotificationSoundSettings.customCommandKey) != nil
let originalCommandValue = defaults.object(forKey: NotificationSoundSettings.customCommandKey)
var deliveredNotificationIDs: [UUID] = []
store.replaceNotificationsForTesting([])
store.configureNotificationDeliveryHandlerForTesting { _, notification in
deliveredNotificationIDs.append(notification.id)
}
appDelegate.tabManager = manager
appDelegate.notificationStore = store
AppFocusState.overrideIsFocused = true
defaults.set("none", forKey: NotificationSoundSettings.key)
defaults.set(
"printf '%s\\n%s\\n%s' \"$CMUX_NOTIFICATION_TITLE\" \"$CMUX_NOTIFICATION_SUBTITLE\" \"$CMUX_NOTIFICATION_BODY\" > '\(commandOutputURL.path)'",
forKey: NotificationSoundSettings.customCommandKey
)
defer {
store.replaceNotificationsForTesting([])
appDelegate.tabManager = originalTabManager
appDelegate.notificationStore = originalNotificationStore
AppFocusState.overrideIsFocused = originalAppFocusOverride
if hadSoundValue {
defaults.set(originalSoundValue, forKey: NotificationSoundSettings.key)
} else {
defaults.removeObject(forKey: NotificationSoundSettings.key)
}
if hadCommandValue {
defaults.set(originalCommandValue, forKey: NotificationSoundSettings.customCommandKey)
} else {
defaults.removeObject(forKey: NotificationSoundSettings.customCommandKey)
}
try? FileManager.default.removeItem(at: commandOutputURL)
}
guard let workspace = manager.selectedWorkspace,
let terminalPanel = workspace.focusedTerminalPanel else {
XCTFail("Expected selected workspace with a focused terminal panel")
return
}
store.addNotification(
tabId: workspace.id,
surfaceId: terminalPanel.id,
title: "",
subtitle: "Focused subtitle",
body: "Focused body"
)
let commandFinished = XCTNSPredicateExpectation(
predicate: NSPredicate { _, _ in
FileManager.default.fileExists(atPath: commandOutputURL.path)
},
object: NSObject()
)
XCTAssertEqual(XCTWaiter().wait(for: [commandFinished], timeout: 2.0), .completed)
XCTAssertTrue(deliveredNotificationIDs.isEmpty)
let output = try String(contentsOf: commandOutputURL, encoding: .utf8)
.trimmingCharacters(in: .whitespacesAndNewlines)
let expectedTitle = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
?? "cmux"
XCTAssertEqual(output.components(separatedBy: "\n"), [expectedTitle, "Focused subtitle", "Focused body"])
}
func testNotificationAuthorizationStateMappingCoversKnownUNAuthorizationStatuses() {