cmux Notification System Architecture: Technical Deep Dive into manaflow-ai/cmux

The cmux notification system architecture implements a three-layer reactive stack where TerminalNotificationStore serves as the central hub, bridging CLI socket commands through macOS UNUserNotificationCenter to SwiftUI interface components, with intelligent suppression logic that prevents notification spam when the user is focused on the target terminal panel.

The cmux notification system architecture enables seamless integration between terminal multiplexing workflows and macOS notification infrastructure. As implemented in the manaflow-ai/cmux repository, this design pattern coordinates socket-based CLI entry points, reactive state management, and system-level notification delivery through a centralized store that maintains consistency across menu bar indicators, sidebar badges, and system alerts.

Three-Layer Architecture Overview

Model and Store Layer

At the foundation lies the Model & Store layer responsible for data persistence and state management. The TerminalNotification struct defines the notification data model, while the TerminalNotificationStore singleton acts as the single source of truth for the entire cmux notification system architecture.

The store maintains the notifications array and manages read/unread states that drive UI elements including the sidebar badge indicators and menu bar pop-over content. As observed by SwiftUI views throughout the application, any mutation to the store immediately triggers interface updates across the workspace sidebar and title-bar accessories.

System Integration Layer

The System Integration layer handles macOS-specific notification infrastructure and permission management. This layer implements the ensureAuthorization method (lines 1266-1284 in Sources/TerminalNotificationStore.swift) to request and monitor notification permissions, routing users to System Settings via openNotificationSettings when access is denied.

The scheduleUserNotification method (lines 1019-1034) constructs UNMutableNotificationContent instances and posts requests to UNUserNotificationCenter. This layer also implements intelligent suppression logic—when cmux is the foreground application focused on the notification's target tab, the system skips external delivery while still executing suppressedNotificationFeedbackHandler (lines 1337-1344) to play sounds and custom commands without displaying system alerts.

UI and Entry Points Layer

The UI & Entry Points layer exposes three distinct interaction patterns. CLI commands including cmux notify, notify_surface, and notify_target parse socket requests in Sources/TerminalController.swift (lines 1708-1715) and route them to the store's addNotification method.

The Menu and Pop-over interface utilizes showNotificationsPopover() defined in Sources/cmuxApp.swift (lines 139-150), while individual notification rows render through the NotificationPopoverRow struct in Sources/Update/UpdateTitlebarAccessory.swift (lines 1140-1195). The Sidebar Badge system queries notificationStore.unreadCount(forTabId:) to color workspace indicators according to unread notification counts.

Notification Flow Pipeline

Step 1: CLI and Socket Entry

Notifications originate through socket commands parsed in TerminalController.swift. The controller implements three entry points:

  • notifyCurrent(args) for general notifications
  • notifySurface for surface-specific alerts
  • notifyTarget for targeted delivery

These methods transform command-line arguments into calls to TerminalNotificationStore.shared.addNotification(...).

Step 2: Store Insertion and Deduplication

The addNotification(tabId:surfaceId:title:subtitle:body:) method (lines 93-114 in TerminalNotificationStore.swift) handles core insertion logic. The store first deduplicates existing notifications for the same tab/surface combination, then determines whether the target panel is currently focused.

If focused, the store calls setFocusedReadIndicator to mark the notification as contextually read. The new TerminalNotification instance prepends to the notifications array, triggering SwiftUI observation updates.

Step 3: Delivery Decision and System Integration

Following insertion, the store evaluates shouldSuppressExternalDelivery. When suppression is active—indicating the user already views the relevant terminal panel—the architecture routes to suppressedNotificationFeedbackHandler, playing the selected sound via NotificationSoundSettings.sound() and executing any custom commands without generating system notifications.

For background or unrelated panels, notificationDeliveryHandler invokes scheduleUserNotification, which ensures authorization before constructing and posting the UNNotificationRequest to macOS notification centers.

Step 4: UI Synchronization

SwiftUI views observe TerminalNotificationStore.notifications through property wrappers. The menu bar utilizes NotificationMenuSnapshotBuilder referenced in cmuxApp.swift to list recent items, while the title-bar pop-over renders entries via NotificationPopoverRow with accessibility identifiers formatted as NotificationPopoverRow.<UUID> for UI testing purposes.

Advanced Features and Configuration

Permission Handling Workflow

When authorization status is denied or undetermined, cmux displays modal prompts directing users to System Settings. The ensureAuthorization(origin: .notificationDelivery) callback pattern ensures graceful degradation when permissions are unavailable, preventing application crashes while maintaining functionality for local sound and command execution.

Custom Sounds and Command Execution

The NotificationSoundSettings struct (lines 32-58) manages audio preferences and custom command execution. Users configure system sounds, custom audio files staged in ~/Library/Sounds, or shell commands that execute on every notification via NotificationSoundSettings.runCustomCommand. All sound preparation runs off-main to preserve UI responsiveness.

Dock Badge Composition

The dockBadgeLabel property (lines 57-74) generates badge strings formatted as <tag>:<count>, respecting the optional tagged run badge setting. This provides at-a-glance unread counts directly on the application dock icon without requiring users to open the notification panel.

Key Source Files

Practical Implementation Examples

// CLI routing in TerminalController.swift
case "notify":
    return notifyCurrent(args)          // Routes to TerminalNotificationStore.shared.addNotification(...)
// Core store insertion with delivery logic
func addNotification(tabId: UUID, surfaceId: UUID?, title: String, subtitle: String, body: String) {
    // Deduplication logic for existing tab/surface notifications...
    let isFocusedPanel = /* App-focus and tab-focus evaluation */
    if isFocusedPanel { setFocusedReadIndicator(forTabId: tabId, surfaceId: surfaceId) }
    
    let notification = TerminalNotification(id: UUID(), tabId: tabId, surfaceId: surfaceId, 
                                          title: title, subtitle: subtitle, body: body, 
                                          timestamp: Date(), isRead: false)
    
    notifications = [notification] + notifications   // Prepend to array
    
    if shouldSuppressExternalDelivery {               
        suppressedNotificationFeedbackHandler(self, notification)  // UI-only feedback
    } else {
        notificationDeliveryHandler(self, notification)            // System notification
    }
}
// System notification scheduling implementation
private func scheduleUserNotification(_ notification: TerminalNotification) {
    ensureAuthorization(origin: .notificationDelivery) { authorized in
        guard authorized else { return }
        let content = UNMutableNotificationContent()
        content.title = resolvedNotificationTitle(for: notification)
        content.subtitle = notification.subtitle
        content.body = notification.body
        content.sound = NotificationSoundSettings.sound()
        content.categoryIdentifier = Self.categoryIdentifier
        // Tab and surface IDs attached to userInfo for routing...
        
        let request = UNNotificationRequest(
            identifier: notification.id.uuidString,
            content: content,
            trigger: nil)
            
        self.center.add(request) { _ in
            NotificationSoundSettings.runCustomCommand(
                title: content.title,
                subtitle: content.subtitle,
                body: content.body)
        }
    }
}
// SwiftUI pop-over row with accessibility support
struct NotificationPopoverRow: View {
    let notification: TerminalNotification
    
    var body: some View {
        Button(action: { openNotification(notification) }) {
            HStack {
                Image(systemName: "bell")
                VStack(alignment: .leading) {
                    Text(notification.title).bold()
                    Text(notification.body).lineLimit(2)
                }
            }
        }
        .accessibilityIdentifier("NotificationPopoverRow.\(notification.id.uuidString)")
    }
}

Summary

  • The cmux notification system architecture separates concerns across three distinct layers: Model & Store, System Integration, and UI Entry Points.
  • TerminalNotificationStore functions as the central reactive hub, implementing the ObservableObject pattern to synchronize state between CLI commands and SwiftUI interfaces.
  • Intelligent suppression logic in shouldSuppressExternalDelivery prevents notification spam when users actively view the target terminal panel, while still executing custom commands and sounds.
  • File-specific implementations in TerminalController.swift (CLI), TerminalNotificationStore.swift (macOS integration), and UpdateTitlebarAccessory.swift (UI) create a testable, maintainable notification pipeline.

Frequently Asked Questions

How does cmux prevent notification spam when the terminal is already focused?

The architecture implements focus-aware suppression in TerminalNotificationStore.swift (lines 1337-1344). When shouldSuppressExternalDelivery evaluates to true—indicating the application is frontmost and the target tab is active—cmux routes notifications through suppressedNotificationFeedbackHandler instead of notificationDeliveryHandler. This executes custom sounds and commands without posting to UNUserNotificationCenter, keeping the UI quiet while the user already views the relevant content.

Which source file handles CLI notification commands?

Socket and CLI parsing for notification commands resides in Sources/TerminalController.swift (lines 1708-1715). This file implements notifyCurrent, notifySurface, and notifyTarget methods that parse arguments from socket clients and route them to TerminalNotificationStore.shared.addNotification(...), serving as the primary entry point for programmatic notification generation.

How are custom notification sounds and commands configured?

The NotificationSoundSettings struct within TerminalNotificationStore.swift (lines 32-58) manages audio selection and command execution. Users select system sounds, custom audio files from ~/Library/Sounds, or shell scripts that execute via NotificationSoundSettings.runCustomCommand. All audio preparation runs on background queues to prevent main-thread blocking, ensuring the terminal interface remains responsive during notification processing.

Where is the notification pop-over UI implemented in the codebase?

The title-bar notification pop-over renders through UpdateTitlebarAccessory.swift (lines 1140-1195), specifically via the NotificationPopoverRow struct. This SwiftUI view generates accessible list items with identifiers formatted as NotificationPopoverRow.<UUID>, while the parent container showNotificationsPopover() in cmuxApp.swift (lines 139-150) manages the pop-over presentation state and positioning relative to the menu bar.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →