Skip to content

Conversation

@kylecarbs
Copy link
Member

Overview

Adds optional notifications when agent streams complete. Works across:

  • Desktop (Electron) - Native notifications with click-to-focus
  • Web Browser - Notifications when tab is backgrounded
  • Mobile PWA - Push notifications when app is closed

Implementation

Backend (NotificationService)

  • Generates and stores VAPID keys for web push authentication
  • Manages push subscriptions per workspace (in-memory)
  • Sends native Electron notifications on desktop
  • Sends web push notifications to subscribed clients on server

IPC Layer

Added 4 new channels:

  • NOTIFICATION_SUBSCRIBE_PUSH - Register push subscription
  • NOTIFICATION_UNSUBSCRIBE_PUSH - Remove push subscription
  • NOTIFICATION_GET_VAPID_KEY - Get public VAPID key
  • NOTIFICATION_SEND - Trigger notification

Frontend

  • Push subscription utilities (src/utils/notifications/pushSubscription.ts)
    • Request notification permission
    • Subscribe via service worker
    • Send subscription to backend
  • Service worker updates (public/service-worker.js)
    • Handle push events
    • Display notifications
    • Handle notification clicks (focus app/window)
  • Preference toggle (Command Palette > Settings)
    • Enable/Disable Completion Notifications
    • Requests permission on enable (web/mobile)
    • Stores preference in localStorage

Trigger Logic

  • WorkspaceStore checks preference on stream-end
  • Only notifies if document is hidden (tab backgrounded) or desktop
  • Uses workspaceId as notification title

Key Design Decisions

Backend vs Frontend Trigger

Frontend triggers notifications after checking localStorage preference. Keeps backend agnostic to UI preferences.

Push Subscription Storage

In-memory Map (not persisted). Frontend re-subscribes on app load if permission already granted. Avoids stale subscriptions.

Global vs Per-Workspace Subscriptions

Global subscriptions (not per-workspace). Simpler UX - one device receives all completion notifications.

VAPID Key Storage

Stored in ~/.cmux/vapid.json. Generated once on first notification enable. Persists across restarts.

Testing

Manual testing checklist:

  • Desktop: Enable notifications, trigger completion, verify notification appears and focuses window on click
  • Web (focused): Enable, trigger completion, verify NO notification (document not hidden)
  • Web (backgrounded): Enable, background tab, trigger completion, verify notification
  • Mobile PWA (open): Same as web (backgrounded)
  • Mobile PWA (closed): Enable, close PWA, trigger completion via server, verify push arrives
  • Permission denied: Enable, deny permission, verify graceful error message
  • Toggle: Disable notifications, trigger completion, verify no notification

Files Changed

New Files

  • src/services/NotificationService.ts (151 lines) - VAPID keys, push notifications
  • src/utils/notifications/pushSubscription.ts (99 lines) - Push subscription management
  • src/types/notification.ts (32 lines) - Type definitions

Modified Files

  • public/service-worker.js (+43 lines) - Push event handlers
  • src/constants/storage.ts (+6 lines) - Notification preference key
  • src/constants/ipc-constants.ts (+4 lines) - IPC channels
  • src/services/ipcMain.ts (+62 lines) - IPC handlers, NotificationService integration
  • src/stores/WorkspaceStore.ts (+13 lines) - Trigger logic
  • src/components/CommandPalette.tsx - Command palette toggle (via sources.ts)
  • src/utils/commands/sources.ts (+40 lines) - Settings section with toggle
  • src/browser/api.ts (+9 lines) - Web API implementation
  • src/preload.ts (+9 lines) - Preload API exposure
  • src/types/ipc.ts (+6 lines) - API type definitions
  • src/App.stories.tsx (+6 lines) - Mock API for Storybook
  • src/main-server.ts (+1 line) - Pass isDesktop=false

Total: ~370 lines added

Future Enhancements

  • Per-workspace notification preferences
  • Notification templates (include model, duration, token count)
  • Smart notifications (only for streams >5min)
  • Sound/vibration preferences

Generated with cmux

- Add NotificationService with VAPID key generation and push support
- Desktop: Native Electron notifications with click-to-focus
- Web: Browser notifications when tab is backgrounded
- Mobile PWA: Push notifications when app is closed
- Command palette toggle: Enable/Disable Completion Notifications
- Notifications trigger on stream-end if enabled and document hidden
- Service worker handles push events and notification clicks
- Push subscriptions managed per-workspace with auto-cleanup

Implementation:
- Backend: NotificationService generates VAPID keys, stores subscriptions
- IPC: 4 new channels (subscribe, unsubscribe, getVapidKey, send)
- Frontend: Push subscription utilities with permission handling
- Service Worker: Push event handlers with notification display
- WorkspaceStore: Checks preference and triggers on stream-end
- Command Palette: Settings section with notification toggle

Generated with `cmux`
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

- Fix subscription filtering to preserve correct indexes
- Move notification trigger to server-side for PWA support
- Remove frontend notification logic (now server-side only)
…ed IPC

- Move desktop notification logic into NotificationService (was broken)
- Desktop notifications now show properly via server-side trigger
- Remove unused NOTIFICATION_SEND IPC channel and handler
- Remove send() method from notification API (not needed)
- Use dynamic import instead of require for electron module
- Update all comments to reflect server-side architecture
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant