Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/AgentNotch/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import AppKit
final class AppDelegate: NSObject, NSApplicationDelegate {
private let store = SessionStore()
private let viewModel = NotchViewModel()
private let vercel = VercelMonitor()
private let vercel = VercelMonitor(notificationsEnabled: { Preferences.vercelNotificationsEnabled })
private var server: HookServer?
private var panelController: NotchPanelController?
private var statusMenu: StatusMenu?
Expand Down
5 changes: 5 additions & 0 deletions Sources/AgentNotch/App/Preferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ enum Preferences {
get { bool("soundOnApproval", true) }
set { save(newValue, "soundOnApproval") }
}
/// Notify when a Vercel deployment reaches a terminal state. Off by default: a build finishing is not your turn.
static var vercelNotificationsEnabled: Bool {
get { bool("vercelNotificationsEnabled", false) }
set { save(newValue, "vercelNotificationsEnabled") }
}
/// 0...1, applied on top of the system volume.
static var soundVolume: Float {
get { Float(number("soundVolume", 0.3)) }
Expand Down
8 changes: 7 additions & 1 deletion Sources/AgentNotch/App/StatusMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ final class StatusMenu: NSObject {
private let languageItem = NSMenuItem(title: L("Language"), action: nil, keyEquivalent: "")
private let vercelItem = NSMenuItem(title: L("Connect Vercel…"), action: nil, keyEquivalent: "")
private let vercelStatusItem = NSMenuItem(title: "", action: nil, keyEquivalent: "")
private let vercelNotifyItem = NSMenuItem(title: L("Notify when a Vercel deployment finishes"), action: nil, keyEquivalent: "")

private static let volumeChoices: [(title: String, value: Double)] = [(L("Quiet"), 0.15), (L("Normal"), 0.3), (L("Loud"), 0.6)]
private static let sleepChoices: [(title: String, value: Double)] = [(L("30 seconds"), 30), (L("1 minute"), 60), (L("3 minutes"), 180)]
Expand Down Expand Up @@ -50,7 +51,8 @@ final class StatusMenu: NSObject {
let connect = NSMenuItem(title: L("Connect agents…"), action: #selector(connectAgents), keyEquivalent: "")
connect.target = self
vercelItem.target = self; vercelItem.action = #selector(connectVercel)
[connect, vercelItem, vercelStatusItem, .separator(), pauseItem, .separator(), soundItem, approvalSoundItem, volumeItem, .separator(), idleItem, sleepItem,
vercelNotifyItem.target = self; vercelNotifyItem.action = #selector(toggleVercelNotifications)
[connect, vercelItem, vercelStatusItem, .separator(), pauseItem, .separator(), soundItem, approvalSoundItem, volumeItem, vercelNotifyItem, .separator(), idleItem, sleepItem,
yourTurnItem, .separator(), languageItem, loginItem, .separator(), quit].forEach(menu.addItem)
menu.delegate = self
item.menu = menu
Expand Down Expand Up @@ -110,6 +112,8 @@ final class StatusMenu: NSObject {

@objc private func toggleSound() { Preferences.soundEnabled.toggle() }

@objc private func toggleVercelNotifications() { Preferences.vercelNotificationsEnabled.toggle() }

@objc private func toggleLogin() {
do {
if SMAppService.mainApp.status == .enabled {
Expand Down Expand Up @@ -171,6 +175,8 @@ extension StatusMenu: NSMenuDelegate {
vercelItem.isHidden = !vercel.configured
vercelStatusItem.isHidden = !vercel.configured || vercel.error == nil
vercelStatusItem.title = vercel.error ?? ""
vercelNotifyItem.isHidden = !vercel.configured
vercelNotifyItem.state = Preferences.vercelNotificationsEnabled ? .on : .off
soundItem.state = Preferences.soundEnabled ? .on : .off
approvalSoundItem.state = Preferences.approvalSoundEnabled ? .on : .off
idleItem.state = Preferences.showIdleMascot ? .on : .off
Expand Down
10 changes: 8 additions & 2 deletions Sources/AgentNotch/Infrastructure/VercelMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ final class VercelMonitor: ObservableObject {
private let worker: URL?
private let slug: String
private let session = URLSession(configuration: .ephemeral, delegate: NoRedirects(), delegateQueue: nil)
/// Whether a finished deployment may post a notification. Injected by the app layer: this monitor reads no preferences.
private let notificationsEnabled: () -> Bool

init() {
init(notificationsEnabled: @escaping () -> Bool = { false }) {
self.notificationsEnabled = notificationsEnabled
if let raw = Bundle.main.object(forInfoDictionaryKey: "VercelWorkerURL") as? String,
let url = URL(string: raw), url.scheme == "https", url.host != nil { worker = url }
else { worker = nil }
Expand Down Expand Up @@ -114,11 +117,14 @@ final class VercelMonitor: ObservableObject {

private func notify(_ deployment: Deployment) async {
guard Bundle.main.bundleURL.pathExtension == "app" else { return }
guard notificationsEnabled() else { return }
let center = UNUserNotificationCenter.current()
guard (try? await center.requestAuthorization(options: [.alert, .sound])) == true else { return }
guard (try? await center.requestAuthorization(options: [.alert])) == true else { return }
let content = UNMutableNotificationContent()
content.title = "Vercel · \(deployment.project)"
content.body = [deployment.status.label, deployment.repository, deployment.commit, deployment.author].compactMap { $0 }.joined(separator: " · ")
// A build finishing is ambient news, not the user's turn: deliver it passively, with no sound or banner that demands attention.
content.interruptionLevel = .passive
try? await center.add(UNNotificationRequest(identifier: "vercel-\(deployment.id)", content: content, trigger: nil))
}

Expand Down
1 change: 1 addition & 0 deletions packaging/locales/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,4 @@
"Ready" = "Bereit";
"Canceled" = "Abgebrochen";
"Unknown" = "Unbekannt";
"Notify when a Vercel deployment finishes" = "Benachrichtigen, wenn ein Vercel-Deployment fertig ist";
1 change: 1 addition & 0 deletions packaging/locales/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,4 @@
"Ready" = "Ready";
"Canceled" = "Canceled";
"Unknown" = "Unknown";
"Notify when a Vercel deployment finishes" = "Notify when a Vercel deployment finishes";
1 change: 1 addition & 0 deletions packaging/locales/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,4 @@
"Ready" = "Listo";
"Canceled" = "Cancelado";
"Unknown" = "Desconocido";
"Notify when a Vercel deployment finishes" = "Notificarme cuando termine un despliegue de Vercel";
Loading