use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Once;
use super::{NotificationSink, SinkNotification};
static IS_SIDECAR: AtomicBool = AtomicBool::new(false);
pub fn set_sidecar_mode(is_sidecar: bool) {
IS_SIDECAR.store(is_sidecar, Ordering::Relaxed);
}
pub fn is_sidecar_mode() -> bool {
IS_SIDECAR.load(Ordering::Relaxed)
}
pub(crate) fn desktop_enabled(configured: Option<bool>, is_sidecar: bool) -> bool {
configured.unwrap_or(!is_sidecar)
}
pub(crate) fn suppressed_by_watcher(category: &str, has_watcher: bool) -> bool {
has_watcher && (category == "run_completed" || category.starts_with("needs_"))
}
#[cfg(target_os = "macos")]
fn ensure_mac_application_set() {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
if let Err(error) = notify_rust::set_application("com.apple.Terminal") {
tracing::warn!(
%error,
"desktop notification sink: failed to set macOS application identity"
);
}
});
}
#[cfg(not(target_os = "macos"))]
fn ensure_mac_application_set() {}
#[cfg(any(all(unix, not(target_os = "macos")), target_os = "windows"))]
fn apply_urgency(notification: &mut notify_rust::Notification, priority: &str) {
let urgency = if priority == "high" {
notify_rust::Urgency::Critical
} else {
notify_rust::Urgency::Normal
};
notification.urgency(urgency);
}
#[cfg(not(any(all(unix, not(target_os = "macos")), target_os = "windows")))]
fn apply_urgency(_notification: &mut notify_rust::Notification, _priority: &str) {}
static WARNED_ONCE: AtomicBool = AtomicBool::new(false);
fn log_failure(error: ¬ify_rust::error::Error) {
if !WARNED_ONCE.swap(true, Ordering::Relaxed) {
tracing::warn!(
%error,
"desktop notification sink: failed to show notification (further failures logged at debug)"
);
} else {
tracing::debug!(%error, "desktop notification sink: failed to show notification");
}
}
fn fire_desktop_notification(title: &str, body: &str, priority: &str) {
ensure_mac_application_set();
let mut notification = notify_rust::Notification::new();
notification.summary(title).body(body);
apply_urgency(&mut notification, priority);
if let Err(error) = notification.show() {
log_failure(&error);
}
}
pub(crate) struct DesktopSink;
impl NotificationSink for DesktopSink {
fn name(&self) -> &'static str {
"desktop"
}
fn deliver(&self, n: &SinkNotification) {
let title = n.title.clone();
let body = n.body.clone();
let priority = n.priority.clone();
tokio::spawn(async move {
if let Err(error) = tokio::task::spawn_blocking(move || {
fire_desktop_notification(&title, &body, &priority)
})
.await
{
tracing::warn!(%error, "desktop notification sink: blocking task panicked/was cancelled");
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn desktop_enabled_auto_is_on_standalone_off_sidecar() {
assert!(desktop_enabled(None, false));
assert!(!desktop_enabled(None, true));
}
#[test]
fn desktop_enabled_explicit_override_wins_either_direction() {
assert!(desktop_enabled(Some(true), true));
assert!(!desktop_enabled(Some(false), false));
}
#[test]
fn sidecar_mode_round_trips_through_the_process_static() {
set_sidecar_mode(true);
assert!(is_sidecar_mode());
set_sidecar_mode(false);
assert!(!is_sidecar_mode());
}
#[test]
fn suppressed_by_watcher_only_for_run_completed_and_needs_categories() {
assert!(suppressed_by_watcher("run_completed", true));
assert!(suppressed_by_watcher("needs_approval", true));
assert!(suppressed_by_watcher("needs_clarification", true));
assert!(!suppressed_by_watcher("run_completed", false));
assert!(!suppressed_by_watcher("needs_approval", false));
assert!(!suppressed_by_watcher("run_failed", true));
assert!(!suppressed_by_watcher("context_critical", true));
assert!(!suppressed_by_watcher("subagent_completed", true));
assert!(!suppressed_by_watcher("background_task_completed", true));
assert!(!suppressed_by_watcher("custom", true));
}
}