/** * NotificationsSection - Notification settings including triggers and ignored repositories. */ import { useState } from 'react'; import { api } from '@renderer/api'; import { RepositoryDropdown, SelectedRepositoryItem, } from '@renderer/components/common/RepositoryDropdown'; import { AlertTriangle, ArrowRightLeft, Bell, BellRing, CheckCircle2, CirclePlus, Clock, ExternalLink, EyeOff, GitBranch, HelpCircle, Inbox, Info, Mail, MessageSquare, PartyPopper, Rocket, Send, ShieldQuestion, Users, Volume2, } from 'lucide-react'; import { SettingRow, SettingsSectionHeader, SettingsSelect, SettingsToggle } from '../components'; import { NotificationTriggerSettings } from '../NotificationTriggerSettings'; import type { RepositoryDropdownItem, SafeConfig } from '../hooks/useSettingsConfig'; import type { NotificationTrigger } from '@renderer/types/data'; import type { TeamReviewState, TeamTaskStatus } from '@shared/types'; /** Notification targets span workflow status plus the explicit review axis. */ type NotifiableStatus = | TeamTaskStatus | Extract; // Snooze duration options const SNOOZE_OPTIONS = [ { value: 15, label: '15 minutes' }, { value: 30, label: '30 minutes' }, { value: 60, label: '1 hour' }, { value: 120, label: '2 hours' }, { value: 240, label: '4 hours' }, { value: -1, label: 'Until tomorrow' }, ] as const; interface NotificationsSectionProps { readonly safeConfig: SafeConfig; readonly saving: boolean; readonly isSnoozed: boolean; readonly ignoredRepositoryItems: RepositoryDropdownItem[]; readonly excludedRepositoryIds: string[]; readonly onNotificationToggle: ( key: | 'enabled' | 'soundEnabled' | 'includeSubagentErrors' | 'notifyOnLeadInbox' | 'notifyOnUserInbox' | 'notifyOnClarifications' | 'notifyOnStatusChange' | 'notifyOnTaskComments' | 'notifyOnTaskCreated' | 'notifyOnAllTasksCompleted' | 'notifyOnCrossTeamMessage' | 'notifyOnTeamLaunched' | 'notifyOnToolApproval' | 'autoResumeOnRateLimit' | 'statusChangeOnlySolo', value: boolean ) => void; readonly onStatusChangeStatusesUpdate: (statuses: string[]) => void; readonly onSnooze: (minutes: number) => Promise; readonly onClearSnooze: () => Promise; readonly onAddIgnoredRepository: (item: RepositoryDropdownItem) => Promise; readonly onRemoveIgnoredRepository: (repositoryId: string) => Promise; readonly onAddTrigger: (trigger: Omit) => Promise; readonly onUpdateTrigger: ( triggerId: string, updates: Partial ) => Promise; readonly onRemoveTrigger: (triggerId: string) => Promise; } export const NotificationsSection = ({ safeConfig, saving, isSnoozed, ignoredRepositoryItems, excludedRepositoryIds, onNotificationToggle, onSnooze, onClearSnooze, onAddIgnoredRepository, onRemoveIgnoredRepository, onAddTrigger, onUpdateTrigger, onRemoveTrigger, onStatusChangeStatusesUpdate, }: NotificationsSectionProps): React.JSX.Element => { const [testStatus, setTestStatus] = useState<'idle' | 'sending' | 'success' | 'error'>('idle'); const [testError, setTestError] = useState(null); const handleTestNotification = async (): Promise => { setTestStatus('sending'); setTestError(null); try { const result = await api.notifications.testNotification(); if (result.success) { setTestStatus('success'); setTimeout(() => setTestStatus('idle'), 3000); } else { setTestStatus('error'); setTestError(result.error ?? 'Unknown error'); setTimeout(() => setTestStatus('idle'), 5000); } } catch (err) { console.error('[notifications] testNotification failed:', err); setTestStatus('error'); const message = err instanceof Error ? err.message : 'Failed to send test notification'; setTestError(message); setTimeout(() => setTestStatus('idle'), 5000); } }; const isDev = import.meta.env.DEV; return (
{/* Dev-mode warning */} {isDev ? (
Dev Mode
Notifications may not work in development mode. macOS identifies the app as "Electron" (bundle ID com.github.Electron) instead of the production app name. Check System Settings → Notifications → Electron to verify permissions.
) : null} {/* Notification Settings */} } /> } > onNotificationToggle('enabled', v)} disabled={saving} /> } > onNotificationToggle('soundEnabled', v)} disabled={saving || !safeConfig.notifications.enabled} /> } > onNotificationToggle('includeSubagentErrors', v)} disabled={saving || !safeConfig.notifications.enabled} /> } >
{testStatus === 'success' ? ( Sent! ) : testStatus === 'error' ? ( {testError} ) : null}
} >
{isSnoozed ? ( ) : ( v !== 0 && onSnooze(v)} disabled={saving || !safeConfig.notifications.enabled} dropUp /> )}
{/* Team Notifications — grouped card */} } />
} > onNotificationToggle('notifyOnLeadInbox', v)} disabled={saving || !safeConfig.notifications.enabled} /> } > onNotificationToggle('notifyOnUserInbox', v)} disabled={saving || !safeConfig.notifications.enabled} /> } > onNotificationToggle('notifyOnClarifications', v)} disabled={saving || !safeConfig.notifications.enabled} /> } > onNotificationToggle('notifyOnTaskComments', v)} disabled={saving || !safeConfig.notifications.enabled} /> } > onNotificationToggle('notifyOnTaskCreated', v)} disabled={saving || !safeConfig.notifications.enabled} /> } > onNotificationToggle('notifyOnAllTasksCompleted', v)} disabled={saving || !safeConfig.notifications.enabled} /> } > onNotificationToggle('notifyOnCrossTeamMessage', v)} disabled={saving || !safeConfig.notifications.enabled} /> } > onNotificationToggle('notifyOnTeamLaunched', v)} disabled={saving || !safeConfig.notifications.enabled} /> } > onNotificationToggle('notifyOnToolApproval', v)} disabled={saving || !safeConfig.notifications.enabled} /> } > onNotificationToggle('autoResumeOnRateLimit', v)} disabled={saving || !safeConfig.notifications.enabled} /> {/* Task Status Change Notifications — nested within team card */}
} > onNotificationToggle('notifyOnStatusChange', v)} disabled={saving || !safeConfig.notifications.enabled} /> {safeConfig.notifications.notifyOnStatusChange && safeConfig.notifications.enabled ? (
Only in Solo mode
Notify only when the team has no teammates
onNotificationToggle('statusChangeOnlySolo', v)} disabled={saving} />
Notify on these statuses
Which target statuses trigger a notification
) : null}
{/* Custom Triggers */} } />

Notifications from these repositories will be ignored

{ignoredRepositoryItems.length > 0 ? (
{ignoredRepositoryItems.map((item) => ( onRemoveIgnoredRepository(item.id)} disabled={saving} /> ))}
) : (

No repositories ignored

)} {/* Task Completion Notifications */} } />

Get native OS notifications when Claude finishes tasks — sounds, banners, and Dock/taskbar badges. Works on macOS, Linux, and Windows.

); }; const STATUS_OPTIONS: { value: NotifiableStatus; label: string }[] = [ { value: 'in_progress', label: 'Started' }, { value: 'completed', label: 'Completed' }, { value: 'review', label: 'Review' }, { value: 'needsFix', label: 'Needs Fixes' }, { value: 'approved', label: 'Approved' }, { value: 'pending', label: 'Pending' }, { value: 'deleted', label: 'Deleted' }, ]; const StatusCheckboxGroup = ({ selected, onChange, disabled, }: { selected: string[]; onChange: (statuses: string[]) => void; disabled: boolean; }) => (
{STATUS_OPTIONS.map((opt) => { const checked = selected.includes(opt.value); return ( ); })}
);