mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
- Move worktree toggle button inside responsive container - Button now properly hides when compact menu is shown - Prevents redundant display of same functionality on mobile
14 KiB
14 KiB
Push Notification Implementation Plan
This document outlines the comprehensive plan for improving VibeTunnel's notification system through two major initiatives:
- Creating a dedicated Notifications tab in macOS settings
- 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:
| Type | Title | Description |
|---|---|---|
| Session Start | Session starts | When a new session starts (useful for shared terminals) |
| Session Exit | Session ends | When a session terminates or crashes (shows exit code) |
| Command Error | Commands fail | When commands fail with non-zero exit codes |
| Command Completion | Commands complete (> 3 seconds) | When commands taking >3 seconds finish (builds, tests, etc.) |
| Terminal Bell | Terminal bell (🔔) | Terminal bell (^G) from vim, IRC mentions, completion sounds |
| Claude Turn | Claude turn notifications | When 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)
- Implement server-side SessionMonitor alongside existing system
- Add new event types to shared types
- Update web UI to show Claude Turn option
Phase 2: Server Enhancement (Non-breaking)
- Deploy enhanced server with SessionMonitor
- Server emits both old and new event formats
- Test with web client to ensure compatibility
Phase 3: Mac App Migration
- Update Mac app to consume server events
- Keep fallback to local monitoring if server unavailable
- Remove local SessionMonitor once stable
Phase 4: Cleanup
- Remove old event formats from server
- Remove local SessionMonitor code from Mac
- 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
- Consistency: Same notifications appear on Mac and web for same events
- Feature Parity: Claude Turn available on both platforms
- Performance: No noticeable lag in notifications
- Reliability: No missed notifications
- 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
| Risk | Impact | Mitigation |
|---|---|---|
| Breaking existing notifications | High | Phased rollout, maintain backwards compatibility |
| Performance impact on server | Medium | Efficient event handling, consider debouncing |
| Mac app offline mode | Medium | Keep local fallback for critical notifications |
| Complex migration | Medium | Detailed testing plan, feature flags |
Conclusion
This two-part implementation will:
- Provide a better UI for notification settings on macOS
- Create a unified notification system across all platforms
- Reduce code duplication and maintenance burden
- Ensure consistent behavior for all users
The migration is designed to be non-breaking with careful phases to minimize risk.