Push Notification Implementation Plan

This document outlines the comprehensive plan for improving VibeTunnel’s notification system through two major initiatives:
  1. Creating a dedicated Notifications tab in macOS settings
  2. Migrating SessionMonitor from the Mac app to the server for unified notifications

Overview

Currently, VibeTunnel has inconsistent notification implementations between the Mac and web clients. The Mac app has its own SessionMonitor while the web relies on server events. This leads to:
  • Different notification behaviors between platforms
  • Missing features (e.g., Claude Turn notifications not shown in web UI)
  • Duplicate code and maintenance burden
  • Inconsistent descriptions and thresholds

Part 1: macOS Settings Redesign

Current State

  • Notification settings are cramped in the General tab
  • No room for descriptive text explaining each notification type
  • Settings are already at 710px height (quite tall)
  • Missing helpful context that exists in the web UI

Proposed Solution: Dedicated Notifications Tab

1. Add Notifications Tab to SettingsTab enum

// SettingsTab.swift
enum SettingsTab: String, CaseIterable {
    case general
    case notifications  // NEW
    case quickStart
    case dashboard
    // ... rest of tabs
}

// Add display name and icon
var displayName: String {
    switch self {
    case .notifications: "Notifications"
    // ... rest
    }
}

var icon: String {
    switch self {
    case .notifications: "bell.badge"
    // ... rest
    }
}

2. Create NotificationSettingsView.swift

struct NotificationSettingsView: View {
    @ObservedObject private var configManager = ConfigManager.shared
    @ObservedObject private var notificationService = NotificationService.shared
    
    var body: some View {
        Form {
            // Master toggle section
            Section {
                VStack(alignment: .leading, spacing: 8) {
                    Toggle("Show Session Notifications", isOn: $showNotifications)
                    Text("Display native macOS notifications for session and command events")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
            }
            
            // Notification types section
            Section {
                NotificationToggleRow(
                    title: "Session starts",
                    description: "When a new session starts (useful for shared terminals)",
                    isOn: $configManager.notificationSessionStart,
                    helpText: NotificationHelp.sessionStart
                )
                
                NotificationToggleRow(
                    title: "Session ends",
                    description: "When a session terminates or crashes (shows exit code)",
                    isOn: $configManager.notificationSessionExit,
                    helpText: NotificationHelp.sessionExit
                )
                
                // ... other notification types
            } header: {
                Text("Notification Types")
            }
            
            // Behavior section
            Section {
                Toggle("Play sound", isOn: $configManager.notificationSoundEnabled)
                Toggle("Show in Notification Center", isOn: $configManager.showInNotificationCenter)
            } header: {
                Text("Notification Behavior")
            }
            
            // Test section
            Section {
                Button("Test Notification") {
                    notificationService.sendTestNotification()
                }
            }
        }
    }
}

3. Create Reusable NotificationToggleRow Component

struct NotificationToggleRow: View {
    let title: String
    let description: String
    @Binding var isOn: Bool
    let helpText: String
    
    var body: some View {
        HStack(alignment: .top, spacing: 12) {
            VStack(alignment: .leading, spacing: 4) {
                HStack {
                    Toggle(title, isOn: $isOn)
                        .toggleStyle(.checkbox)
                    HelpTooltip(text: helpText)
                }
                Text(description)
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
        }
        .padding(.vertical, 4)
    }
}

4. Update SettingsView.swift

// Add the new tab
NotificationSettingsView()
    .tabItem {
        Label(SettingsTab.notifications.displayName, 
              systemImage: SettingsTab.notifications.icon)
    }
    .tag(SettingsTab.notifications)

5. Update GeneralSettingsView.swift

Remove all notification-related settings to free up space.

Standardized Notification Descriptions

Use these descriptions consistently across Mac and web:
TypeTitleDescription
Session StartSession startsWhen a new session starts (useful for shared terminals)
Session ExitSession endsWhen a session terminates or crashes (shows exit code)
Command ErrorCommands failWhen commands fail with non-zero exit codes
Command CompletionCommands complete (> 3 seconds)When commands taking >3 seconds finish (builds, tests, etc.)
Terminal BellTerminal bell (🔔)Terminal bell (^G) from vim, IRC mentions, completion sounds
Claude TurnClaude turn notificationsWhen Claude AI finishes responding and awaits input

Part 2: Server-Side SessionMonitor Migration

Current Architecture

Mac App:
  SessionMonitor (Swift) → NotificationService → macOS notifications
  
Server:
  PtyManager → Basic events → SSE → Web notifications

Proposed Architecture

Server:
  PtyManager → SessionMonitor (TypeScript) → Enhanced events → SSE/WebSocket

                                                        Mac & Web clients

Implementation Steps

1. Create Server-Side SessionMonitor

// web/src/server/services/session-monitor.ts

export interface SessionState {
  id: string;
  name: string;
  command: string[];
  isRunning: boolean;
  activityStatus?: {
    isActive: boolean;
    lastActivity?: Date;
    specificStatus?: {
      app: string;
      status: string;
    };
  };
  commandStartTime?: Date;
  lastCommand?: string;
}

export class SessionMonitor {
  private sessions = new Map<string, SessionState>();
  private claudeIdleNotified = new Set<string>();
  private lastActivityState = new Map<string, boolean>();
  private commandThresholdMs = 3000; // 3 seconds
  
  constructor(
    private ptyManager: PtyManager,
    private eventBus: EventEmitter
  ) {
    this.setupEventListeners();
  }
  
  private detectClaudeSession(session: SessionState): boolean {
    const isClaudeCommand = session.command
      .join(' ')
      .toLowerCase()
      .includes('claude');
    
    const isClaudeApp = session.activityStatus?.specificStatus?.app
      .toLowerCase()
      .includes('claude') ?? false;
      
    return isClaudeCommand || isClaudeApp;
  }
  
  private checkClaudeTurnNotification(sessionId: string, newState: SessionState) {
    if (!this.detectClaudeSession(newState)) return;
    
    const currentActive = newState.activityStatus?.isActive ?? false;
    const previousActive = this.lastActivityState.get(sessionId) ?? false;
    
    // Claude went from active to idle
    if (previousActive && !currentActive && !this.claudeIdleNotified.has(sessionId)) {
      this.eventBus.emit('notification', {
        type: ServerEventType.ClaudeTurn,
        sessionId,
        sessionName: newState.name,
        message: 'Claude has finished responding'
      });
      this.claudeIdleNotified.add(sessionId);
    }
    
    // Reset when Claude becomes active again
    if (!previousActive && currentActive) {
      this.claudeIdleNotified.delete(sessionId);
    }
    
    this.lastActivityState.set(sessionId, currentActive);
  }
  
  // ... other monitoring methods
}

2. Enhance Event Types

// web/src/shared/types.ts

export enum ServerEventType {
  SessionStart = 'session-start',
  SessionExit = 'session-exit',
  CommandFinished = 'command-finished',
  CommandError = 'command-error',  // NEW - separate from finished
  Bell = 'bell',                   // NEW
  ClaudeTurn = 'claude-turn',
  Connected = 'connected'
}

export interface ServerEvent {
  type: ServerEventType;
  timestamp: string;
  sessionId: string;
  sessionName?: string;
  
  // Event-specific data
  exitCode?: number;
  command?: string;
  duration?: number;
  message?: string;
  
  // Activity status for richer client UI
  activityStatus?: {
    isActive: boolean;
    app?: string;
  };
}

3. Integrate with PtyManager

// web/src/server/pty/pty-manager.ts

class PtyManager {
  private sessionMonitor: SessionMonitor;
  
  constructor() {
    this.sessionMonitor = new SessionMonitor(this, serverEventBus);
  }
  
  // Feed data to SessionMonitor
  private handlePtyData(sessionId: string, data: string) {
    // Existing data handling...
    
    // Detect bell character
    if (data.includes('\x07')) {
      serverEventBus.emit('notification', {
        type: ServerEventType.Bell,
        sessionId,
        sessionName: this.sessions.get(sessionId)?.name
      });
    }
    
    // Update activity status
    this.sessionMonitor.updateActivity(sessionId, {
      isActive: true,
      lastActivity: new Date()
    });
  }
}

4. Update Server Routes

// web/src/server/routes/events.ts

// Enhanced event handling
serverEventBus.on('notification', (event: ServerEvent) => {
  // Send to all connected SSE clients
  broadcastEvent(event);
  
  // Log for debugging
  logger.info(`📢 Notification event: ${event.type} for session ${event.sessionId}`);
});

5. Update Mac NotificationService

// NotificationService.swift

class NotificationService {
    // Remove local SessionMonitor dependency
    // Subscribe to server SSE events instead
    
    private func connectToServerEvents() {
        eventSource = EventSource(url: "http://localhost:4020/api/events")
        
        eventSource.onMessage { event in
            guard let data = event.data,
                  let serverEvent = try? JSONDecoder().decode(ServerEvent.self, from: data) else {
                return
            }
            
            Task { @MainActor in
                self.handleServerEvent(serverEvent)
            }
        }
    }
    
    private func handleServerEvent(_ event: ServerEvent) {
        // Map server events to notifications
        switch event.type {
        case .sessionStart:
            if preferences.sessionStart {
                sendNotification(for: event)
            }
        // ... handle other event types
        }
    }
}

6. Update Web Notification Service

// web/src/client/services/push-notification-service.ts

// Add Claude Turn to notification handling
private handleServerEvent(event: ServerEvent) {
  if (!this.preferences[this.mapEventTypeToPreference(event.type)]) {
    return;
  }
  
  // Send browser notification
  this.showNotification(event);
}

private mapEventTypeToPreference(type: ServerEventType): keyof NotificationPreferences {
  const mapping = {
    [ServerEventType.SessionStart]: 'sessionStart',
    [ServerEventType.SessionExit]: 'sessionExit',
    [ServerEventType.CommandFinished]: 'commandCompletion',
    [ServerEventType.CommandError]: 'commandError',
    [ServerEventType.Bell]: 'bell',
    [ServerEventType.ClaudeTurn]: 'claudeTurn'  // Now properly mapped
  };
  return mapping[type];
}

7. Add Claude Turn to Web UI

// web/src/client/components/settings.ts

// In notification types section
${this.renderNotificationToggle('claudeTurn', 'Claude Turn', 
  'When Claude AI finishes responding and awaits input')}

Migration Strategy

Phase 1: Preparation (Non-breaking)

  1. Implement server-side SessionMonitor alongside existing system
  2. Add new event types to shared types
  3. Update web UI to show Claude Turn option

Phase 2: Server Enhancement (Non-breaking)

  1. Deploy enhanced server with SessionMonitor
  2. Server emits both old and new event formats
  3. Test with web client to ensure compatibility

Phase 3: Mac App Migration

  1. Update Mac app to consume server events
  2. Keep fallback to local monitoring if server unavailable
  3. Remove local SessionMonitor once stable

Phase 4: Cleanup

  1. Remove old event formats from server
  2. Remove local SessionMonitor code from Mac
  3. Document new architecture

Testing Plan

Unit Tests

  • SessionMonitor Claude detection logic
  • Event threshold calculations
  • Activity state transitions

Integration Tests

  • Server events reach both Mac and web clients
  • Notification preferences are respected
  • Claude Turn notifications work correctly
  • Bell character detection

Manual Testing

  • Test each notification type on both platforms
  • Verify descriptions match
  • Test with multiple clients connected
  • Test offline Mac app behavior

Success Metrics

  1. Consistency: Same notifications appear on Mac and web for same events
  2. Feature Parity: Claude Turn available on both platforms
  3. Performance: No noticeable lag in notifications
  4. Reliability: No missed notifications
  5. Maintainability: Single codebase for monitoring logic

Timeline Estimate

  • Week 1: Implement macOS Notifications tab
  • Week 2: Create server-side SessionMonitor
  • Week 3: Integrate and test with web client
  • Week 4: Migrate Mac app and testing
  • Week 5: Polish, documentation, and deployment

Risks and Mitigations

RiskImpactMitigation
Breaking existing notificationsHighPhased rollout, maintain backwards compatibility
Performance impact on serverMediumEfficient event handling, consider debouncing
Mac app offline modeMediumKeep local fallback for critical notifications
Complex migrationMediumDetailed testing plan, feature flags

Conclusion

This two-part implementation will:
  1. Provide a better UI for notification settings on macOS
  2. Create a unified notification system across all platforms
  3. Reduce code duplication and maintenance burden
  4. Ensure consistent behavior for all users
The migration is designed to be non-breaking with careful phases to minimize risk.