feat: add configurable web native notifications for assistant completion (#123)
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { isWebRuntime } from '@/lib/desktop';
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const NotificationSettings: React.FC = () => {
|
||||
const nativeNotificationsEnabled = useUIStore(state => state.nativeNotificationsEnabled);
|
||||
const setNativeNotificationsEnabled = useUIStore(state => state.setNativeNotificationsEnabled);
|
||||
|
||||
const [notificationPermission, setNotificationPermission] = React.useState<NotificationPermission>('default');
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof Notification !== 'undefined') {
|
||||
setNotificationPermission(Notification.permission);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleToggleChange = async (checked: boolean) => {
|
||||
if (checked && typeof Notification !== 'undefined' && Notification.permission === 'default') {
|
||||
try {
|
||||
const permission = await Notification.requestPermission();
|
||||
setNotificationPermission(permission);
|
||||
if (permission === 'granted') {
|
||||
setNativeNotificationsEnabled(true);
|
||||
} else {
|
||||
toast.error('Notification permission denied', {
|
||||
description: 'Please enable notifications in your browser settings.',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to request notification permission:', error);
|
||||
toast.error('Failed to request notification permission');
|
||||
}
|
||||
} else if (checked && notificationPermission === 'granted') {
|
||||
setNativeNotificationsEnabled(true);
|
||||
} else {
|
||||
setNativeNotificationsEnabled(false);
|
||||
}
|
||||
};
|
||||
|
||||
const canShowNotifications = typeof Notification !== 'undefined' && Notification.permission === 'granted';
|
||||
|
||||
if (!isWebRuntime()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">
|
||||
Native Notifications
|
||||
</h3>
|
||||
<p className="typography-ui text-muted-foreground">
|
||||
Show browser notifications when an assistant completes a task.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="typography-ui text-foreground">
|
||||
Enable native notifications
|
||||
</span>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={nativeNotificationsEnabled && canShowNotifications}
|
||||
onChange={(e) => handleToggleChange(e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-neutral-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary/50 dark:peer-focus:ring-primary/50 rounded-full peer dark:bg-neutral-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-neutral-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-neutral-600 peer-checked:bg-primary" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{notificationPermission === 'denied' && (
|
||||
<p className="typography-micro text-destructive">
|
||||
Notification permission denied. Enable notifications in your browser settings.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{notificationPermission === 'granted' && !nativeNotificationsEnabled && (
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
Notifications are enabled in your browser. Toggle the switch above to activate them.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { AboutSettings } from './AboutSettings';
|
||||
import { SessionRetentionSettings } from './SessionRetentionSettings';
|
||||
import { DefaultsSettings } from './DefaultsSettings';
|
||||
import { WorktreeSectionContent } from './WorktreeSectionContent';
|
||||
import { NotificationSettings } from './NotificationSettings';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { isWebRuntime } from '@/lib/desktop';
|
||||
@@ -53,6 +54,8 @@ export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) =>
|
||||
return <SessionsSectionContent />;
|
||||
case 'worktree':
|
||||
return <WorktreeSectionContent />;
|
||||
case 'notifications':
|
||||
return <NotificationSectionContent />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -90,3 +93,8 @@ const SessionsSectionContent: React.FC = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Notifications section: Native browser notifications
|
||||
const NotificationSectionContent: React.FC = () => {
|
||||
return <NotificationSettings />;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
||||
import { AboutSettings } from './AboutSettings';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type OpenChamberSection = 'visual' | 'chat' | 'sessions' | 'worktree';
|
||||
export type OpenChamberSection = 'visual' | 'chat' | 'sessions' | 'worktree' | 'notifications';
|
||||
|
||||
interface OpenChamberSidebarProps {
|
||||
selectedSection: OpenChamberSection;
|
||||
@@ -39,6 +39,11 @@ const OPENCHAMBER_SECTION_GROUPS: SectionGroup[] = [
|
||||
label: 'Worktree',
|
||||
items: ['Branch', 'Setup'],
|
||||
},
|
||||
{
|
||||
id: 'notifications',
|
||||
label: 'Notifications',
|
||||
items: ['Native'],
|
||||
},
|
||||
];
|
||||
|
||||
export const OpenChamberSidebar: React.FC<OpenChamberSidebarProps> = ({
|
||||
|
||||
Reference in New Issue
Block a user