@@ -13,6 +13,7 @@ import (
1313 "os"
1414 "path/filepath"
1515 "regexp"
16+ "runtime"
1617 "slices"
1718 "strconv"
1819 "strings"
@@ -436,31 +437,64 @@ func (m *UI) sendNotification(n notification.Notification) tea.Cmd {
436437}
437438
438439// selectNotificationBackend chooses the appropriate notification backend based
439−// on terminal capabilities and environment. This is a pure function that should
440−// be called once during initialization or when capabilities change.
441−func selectNotificationBackend(caps common.Capabilities) notification.Backend {
440+// on terminal capabilities, environment, and user configuration. This is a pure
441+// function that should be called once during initialization or when capabilities
442+// change.
443+func selectNotificationBackend(caps common.Capabilities, cfg *config.Config) notification.Backend {
444+ // Check for explicit user preference first.
445+ if cfg != nil && cfg.Options != nil && cfg.Options.NotificationStyle != "" {
446+ switch cfg.Options.NotificationStyle {
447+ case "native":
448+ slog.Debug("Using native backend (user preference)")
449+ return notification.NewNativeBackend(notification.Icon)
450+ case "osc":
451+ slog.Debug("Using OSC backend (user preference)", "osc99_supported", caps.OSC99Notifications)
452+ return notification.NewOSCBackend(notification.Icon, caps.OSC99Notifications)
453+ case "bell":
454+ slog.Debug("Using bell backend (user preference)")
455+ return notification.NewBellBackend()
456+ case "disabled":
457+ slog.Debug("Notifications disabled (user preference)")
458+ return notification.NoopBackend{}
459+ case "auto":
460+ // Fall through to auto-detection below.
461+ default:
462+ slog.Warn("Unknown notification style, using auto", "style", cfg.Options.NotificationStyle)
463+ }
464+ }
465+
466+ // Auto-detect based on environment and capabilities.
442467 _, isSSH := caps.Env.LookupEnv("SSH_TTY")
443468
444469 // SSH sessions use terminal-based notifications (OSC 99 or 777).
445470 if isSSH {
446− if caps.OSC99Notifications {
447− return notification.NewOSC99Backend(notification.Icon)
448− }
449− return notification.NewOSC777Backend()
471+ slog.Debug("Selected OSCBackend for SSH session", "osc99_supported", caps.OSC99Notifications)
472+ return notification.NewOSCBackend(notification.Icon, caps.OSC99Notifications)
450473 }
451474
452− // Local sessions use native OS notifications if focus events are supported.
475+ // Local sessions: prefer OSC on macOS because the native backend (beeep)
476+ // uses terminal-notifier or AppleScript, which is slow and doesn't display
477+ // icons properly. OSC 99 provides a more polished experience with icon support.
478+ if runtime.GOOS == "darwin" {
479+ slog.Debug("Selected OSCBackend for local macOS session", "osc99_supported", caps.OSC99Notifications)
480+ return notification.NewOSCBackend(notification.Icon, caps.OSC99Notifications)
481+ }
482+
483+ // Non-macOS local sessions use native OS notifications if focus events are supported.
453484 // Without focus events, we can't suppress notifications when focused, so
454485 // we disable them entirely to avoid spamming the user.
455486 if caps.ReportFocusEvents {
487+ slog.Debug("Selected NativeBackend for local session")
456488 return notification.NewNativeBackend(notification.Icon)
457489 }
458490
491+ slog.Debug("Selected NoopBackend (focus events not supported)")
459492 return notification.NoopBackend{}
460493}
461494
462495func (m *UI) updateNotificationBackend() {
463− m.notifyBackend = selectNotificationBackend(m.caps)
496+ cfg := m.com.Config()
497+ m.notifyBackend = selectNotificationBackend(m.caps, cfg)
464498}
465499
466500// shouldSendNotification returns true if notifications should be sent based on
@@ -471,6 +505,10 @@ func (m *UI) shouldSendNotification() bool {
471505 if cfg != nil && cfg.Options != nil && cfg.Options.DisableNotifications {
472506 return false
473507 }
508+ // If the user explicitly set style to "disabled", skip sending.
509+ if cfg != nil && cfg.Options != nil && cfg.Options.NotificationStyle == "disabled" {
510+ return false
511+ }
474512 return m.caps.ReportFocusEvents && !m.notifyWindowFocused
475513}
476514