mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-26 08:17:24 +00:00
feat: introduce notification system and claude notifications (#450)
* feat: introduce notification system and claude notifications * fix(sw): prevent caching of API requests and WebSocket upgrades * default to false for webpush notifications and translations for the button * fix: notifications orchestrator and add a notification when first enabled * fix: remove unused state update and dependency in settings controller hook * fix: show notifications settings tab * fix: add notifications for response completion for all providers * feat: show session name in notification and don't reload tab on clicking --- the notification --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Haileyesus <something@gmail.com>
This commit is contained in:
@@ -72,6 +72,40 @@ export default function AppContent() {
|
||||
};
|
||||
}, [openSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const handleServiceWorkerMessage = (event: MessageEvent) => {
|
||||
const message = event.data;
|
||||
if (!message || message.type !== 'notification:navigate') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof message.provider === 'string' && message.provider.trim()) {
|
||||
localStorage.setItem('selected-provider', message.provider);
|
||||
}
|
||||
|
||||
setActiveTab('chat');
|
||||
setSidebarOpen(false);
|
||||
void refreshProjectsSilently();
|
||||
|
||||
if (typeof message.sessionId === 'string' && message.sessionId) {
|
||||
navigate(`/session/${message.sessionId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage);
|
||||
|
||||
return () => {
|
||||
navigator.serviceWorker.removeEventListener('message', handleServiceWorkerMessage);
|
||||
};
|
||||
}, [navigate, refreshProjectsSilently, setActiveTab, setSidebarOpen]);
|
||||
|
||||
// Permission recovery: query pending permissions on WebSocket reconnect or session change
|
||||
useEffect(() => {
|
||||
const isReconnect = isConnected && !wasConnectedRef.current;
|
||||
|
||||
@@ -82,6 +82,24 @@ const createFakeSubmitEvent = () => {
|
||||
const isTemporarySessionId = (sessionId: string | null | undefined) =>
|
||||
Boolean(sessionId && sessionId.startsWith('new-session-'));
|
||||
|
||||
const getNotificationSessionSummary = (
|
||||
selectedSession: ProjectSession | null,
|
||||
fallbackInput: string,
|
||||
): string | null => {
|
||||
const sessionSummary = selectedSession?.summary || selectedSession?.name || selectedSession?.title;
|
||||
if (typeof sessionSummary === 'string' && sessionSummary.trim()) {
|
||||
const normalized = sessionSummary.replace(/\s+/g, ' ').trim();
|
||||
return normalized.length > 80 ? `${normalized.slice(0, 77)}...` : normalized;
|
||||
}
|
||||
|
||||
const normalizedFallback = fallbackInput.replace(/\s+/g, ' ').trim();
|
||||
if (!normalizedFallback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalizedFallback.length > 80 ? `${normalizedFallback.slice(0, 77)}...` : normalizedFallback;
|
||||
};
|
||||
|
||||
export function useChatComposerState({
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
@@ -603,6 +621,7 @@ export function useChatComposerState({
|
||||
|
||||
const toolsSettings = getToolsSettings();
|
||||
const resolvedProjectPath = selectedProject.fullPath || selectedProject.path || '';
|
||||
const sessionSummary = getNotificationSessionSummary(selectedSession, currentInput);
|
||||
|
||||
if (provider === 'cursor') {
|
||||
sendMessage({
|
||||
@@ -616,6 +635,7 @@ export function useChatComposerState({
|
||||
resume: Boolean(effectiveSessionId),
|
||||
model: cursorModel,
|
||||
skipPermissions: toolsSettings?.skipPermissions || false,
|
||||
sessionSummary,
|
||||
toolsSettings,
|
||||
},
|
||||
});
|
||||
@@ -630,6 +650,7 @@ export function useChatComposerState({
|
||||
sessionId: effectiveSessionId,
|
||||
resume: Boolean(effectiveSessionId),
|
||||
model: codexModel,
|
||||
sessionSummary,
|
||||
permissionMode: permissionMode === 'plan' ? 'default' : permissionMode,
|
||||
},
|
||||
});
|
||||
@@ -644,6 +665,7 @@ export function useChatComposerState({
|
||||
sessionId: effectiveSessionId,
|
||||
resume: Boolean(effectiveSessionId),
|
||||
model: geminiModel,
|
||||
sessionSummary,
|
||||
permissionMode,
|
||||
toolsSettings,
|
||||
},
|
||||
@@ -660,6 +682,7 @@ export function useChatComposerState({
|
||||
toolsSettings,
|
||||
permissionMode,
|
||||
model: claudeModel,
|
||||
sessionSummary,
|
||||
images: uploadedImages,
|
||||
},
|
||||
});
|
||||
@@ -681,6 +704,7 @@ export function useChatComposerState({
|
||||
safeLocalStorage.removeItem(`draft_input_${selectedProject.name}`);
|
||||
},
|
||||
[
|
||||
selectedSession,
|
||||
attachedImages,
|
||||
claudeModel,
|
||||
codexModel,
|
||||
@@ -697,7 +721,6 @@ export function useChatComposerState({
|
||||
resetCommandMenuState,
|
||||
scrollToBottom,
|
||||
selectedProject,
|
||||
selectedSession?.id,
|
||||
sendMessage,
|
||||
setCanAbortSession,
|
||||
setChatMessages,
|
||||
|
||||
@@ -80,7 +80,7 @@ export const ToolRenderer: React.FC<ToolRendererProps> = memo(({
|
||||
}
|
||||
}, [displayConfig, parsedData, onFileOpen]);
|
||||
|
||||
// Route subagent containers to dedicated component (after hooks to keep call order stable)
|
||||
// Route subagent containers to dedicated component (after hooks to satisfy Rules of Hooks)
|
||||
if (isSubagentContainer && subagentState) {
|
||||
if (mode === 'result') {
|
||||
return null;
|
||||
|
||||
@@ -18,6 +18,7 @@ export const SETTINGS_MAIN_TABS: SettingsMainTab[] = [
|
||||
'git',
|
||||
'api',
|
||||
'tasks',
|
||||
'notifications',
|
||||
];
|
||||
|
||||
export const AGENT_PROVIDERS: AgentProvider[] = ['claude', 'cursor', 'codex'];
|
||||
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
McpServer,
|
||||
McpToolsResult,
|
||||
McpTestResult,
|
||||
NotificationPreferencesState,
|
||||
ProjectSortOrder,
|
||||
SettingsMainTab,
|
||||
SettingsProject,
|
||||
@@ -96,9 +97,14 @@ type CodexSettingsStorage = {
|
||||
permissionMode?: CodexPermissionMode;
|
||||
};
|
||||
|
||||
type NotificationPreferencesResponse = {
|
||||
success?: boolean;
|
||||
preferences?: NotificationPreferencesState;
|
||||
};
|
||||
|
||||
type ActiveLoginProvider = AgentProvider | '';
|
||||
|
||||
const KNOWN_MAIN_TABS: SettingsMainTab[] = ['agents', 'appearance', 'git', 'api', 'tasks', 'plugins'];
|
||||
const KNOWN_MAIN_TABS: SettingsMainTab[] = ['agents', 'appearance', 'git', 'api', 'tasks', 'notifications', 'plugins'];
|
||||
|
||||
const normalizeMainTab = (tab: string): SettingsMainTab => {
|
||||
// Keep backwards compatibility with older callers that still pass "tools".
|
||||
@@ -186,6 +192,18 @@ const createEmptyCursorPermissions = (): CursorPermissionsState => ({
|
||||
...DEFAULT_CURSOR_PERMISSIONS,
|
||||
});
|
||||
|
||||
const createDefaultNotificationPreferences = (): NotificationPreferencesState => ({
|
||||
channels: {
|
||||
inApp: true,
|
||||
webPush: false,
|
||||
},
|
||||
events: {
|
||||
actionRequired: true,
|
||||
stop: true,
|
||||
error: true,
|
||||
},
|
||||
});
|
||||
|
||||
export function useSettingsController({ isOpen, initialTab, projects, onClose }: UseSettingsControllerArgs) {
|
||||
const { isDarkMode, toggleDarkMode } = useTheme() as ThemeContextValue;
|
||||
const closeTimerRef = useRef<number | null>(null);
|
||||
@@ -204,6 +222,9 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
const [cursorPermissions, setCursorPermissions] = useState<CursorPermissionsState>(() => (
|
||||
createEmptyCursorPermissions()
|
||||
));
|
||||
const [notificationPreferences, setNotificationPreferences] = useState<NotificationPreferencesState>(() => (
|
||||
createDefaultNotificationPreferences()
|
||||
));
|
||||
const [codexPermissionMode, setCodexPermissionMode] = useState<CodexPermissionMode>('default');
|
||||
const [geminiPermissionMode, setGeminiPermissionMode] = useState<GeminiPermissionMode>('default');
|
||||
|
||||
@@ -670,6 +691,22 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
);
|
||||
setGeminiPermissionMode(savedGeminiSettings.permissionMode || 'default');
|
||||
|
||||
try {
|
||||
const notificationResponse = await authenticatedFetch('/api/settings/notification-preferences');
|
||||
if (notificationResponse.ok) {
|
||||
const notificationData = await toResponseJson<NotificationPreferencesResponse>(notificationResponse);
|
||||
if (notificationData.success && notificationData.preferences) {
|
||||
setNotificationPreferences(notificationData.preferences);
|
||||
} else {
|
||||
setNotificationPreferences(createDefaultNotificationPreferences());
|
||||
}
|
||||
} else {
|
||||
setNotificationPreferences(createDefaultNotificationPreferences());
|
||||
}
|
||||
} catch {
|
||||
setNotificationPreferences(createDefaultNotificationPreferences());
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
fetchMcpServers(),
|
||||
fetchCursorMcpServers(),
|
||||
@@ -679,6 +716,7 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
console.error('Error loading settings:', error);
|
||||
setClaudePermissions(createEmptyClaudePermissions());
|
||||
setCursorPermissions(createEmptyCursorPermissions());
|
||||
setNotificationPreferences(createDefaultNotificationPreferences());
|
||||
setCodexPermissionMode('default');
|
||||
setProjectSortOrder('name');
|
||||
}
|
||||
@@ -699,7 +737,9 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
void checkAuthStatus(loginProvider);
|
||||
}, [checkAuthStatus, loginProvider]);
|
||||
|
||||
const saveSettings = useCallback(() => {
|
||||
const saveSettings = useCallback(async () => {
|
||||
setSaveStatus(null);
|
||||
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
localStorage.setItem('claude-settings', JSON.stringify({
|
||||
@@ -727,6 +767,14 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
lastUpdated: now,
|
||||
}));
|
||||
|
||||
const notificationResponse = await authenticatedFetch('/api/settings/notification-preferences', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(notificationPreferences),
|
||||
});
|
||||
if (!notificationResponse.ok) {
|
||||
throw new Error('Failed to save notification preferences');
|
||||
}
|
||||
|
||||
setSaveStatus('success');
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
@@ -740,6 +788,7 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
cursorPermissions.allowedCommands,
|
||||
cursorPermissions.disallowedCommands,
|
||||
cursorPermissions.skipPermissions,
|
||||
notificationPreferences,
|
||||
geminiPermissionMode,
|
||||
projectSortOrder,
|
||||
]);
|
||||
@@ -862,6 +911,8 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
setClaudePermissions,
|
||||
cursorPermissions,
|
||||
setCursorPermissions,
|
||||
notificationPreferences,
|
||||
setNotificationPreferences,
|
||||
codexPermissionMode,
|
||||
setCodexPermissionMode,
|
||||
mcpServers,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'plugins';
|
||||
export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'notifications' | 'plugins';
|
||||
export type AgentProvider = 'claude' | 'cursor' | 'codex' | 'gemini';
|
||||
export type AgentCategory = 'account' | 'permissions' | 'mcp';
|
||||
export type ProjectSortOrder = 'name' | 'date';
|
||||
@@ -106,6 +106,18 @@ export type ClaudePermissionsState = {
|
||||
skipPermissions: boolean;
|
||||
};
|
||||
|
||||
export type NotificationPreferencesState = {
|
||||
channels: {
|
||||
inApp: boolean;
|
||||
webPush: boolean;
|
||||
};
|
||||
events: {
|
||||
actionRequired: boolean;
|
||||
stop: boolean;
|
||||
error: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type CursorPermissionsState = {
|
||||
allowedCommands: string[];
|
||||
disallowedCommands: string[];
|
||||
|
||||
@@ -9,9 +9,11 @@ import AgentsSettingsTab from '../view/tabs/agents-settings/AgentsSettingsTab';
|
||||
import AppearanceSettingsTab from '../view/tabs/AppearanceSettingsTab';
|
||||
import CredentialsSettingsTab from '../view/tabs/api-settings/CredentialsSettingsTab';
|
||||
import GitSettingsTab from '../view/tabs/git-settings/GitSettingsTab';
|
||||
import NotificationsSettingsTab from '../view/tabs/NotificationsSettingsTab';
|
||||
import TasksSettingsTab from '../view/tabs/tasks-settings/TasksSettingsTab';
|
||||
import PluginSettingsTab from '../../plugins/view/PluginSettingsTab';
|
||||
import { useSettingsController } from '../hooks/useSettingsController';
|
||||
import { useWebPush } from '../../../hooks/useWebPush';
|
||||
import type { SettingsProps } from '../types/types';
|
||||
|
||||
function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: SettingsProps) {
|
||||
@@ -27,6 +29,8 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
||||
updateCodeEditorSetting,
|
||||
claudePermissions,
|
||||
setClaudePermissions,
|
||||
notificationPreferences,
|
||||
setNotificationPreferences,
|
||||
cursorPermissions,
|
||||
setCursorPermissions,
|
||||
codexPermissionMode,
|
||||
@@ -70,6 +74,32 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
||||
onClose,
|
||||
});
|
||||
|
||||
const {
|
||||
permission: pushPermission,
|
||||
isSubscribed: isPushSubscribed,
|
||||
isLoading: isPushLoading,
|
||||
subscribe: pushSubscribe,
|
||||
unsubscribe: pushUnsubscribe,
|
||||
} = useWebPush();
|
||||
|
||||
const handleEnablePush = async () => {
|
||||
await pushSubscribe();
|
||||
// Server sets webPush: true in preferences on subscribe; sync local state
|
||||
setNotificationPreferences({
|
||||
...notificationPreferences,
|
||||
channels: { ...notificationPreferences.channels, webPush: true },
|
||||
});
|
||||
};
|
||||
|
||||
const handleDisablePush = async () => {
|
||||
await pushUnsubscribe();
|
||||
// Server sets webPush: false in preferences on unsubscribe; sync local state
|
||||
setNotificationPreferences({
|
||||
...notificationPreferences,
|
||||
channels: { ...notificationPreferences.channels, webPush: false },
|
||||
});
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
@@ -161,6 +191,18 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
||||
|
||||
{activeTab === 'tasks' && <TasksSettingsTab />}
|
||||
|
||||
{activeTab === 'notifications' && (
|
||||
<NotificationsSettingsTab
|
||||
notificationPreferences={notificationPreferences}
|
||||
onNotificationPreferencesChange={setNotificationPreferences}
|
||||
pushPermission={pushPermission}
|
||||
isPushSubscribed={isPushSubscribed}
|
||||
isPushLoading={isPushLoading}
|
||||
onEnablePush={handleEnablePush}
|
||||
onDisablePush={handleDisablePush}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'api' && <CredentialsSettingsTab />}
|
||||
|
||||
{activeTab === 'plugins' && <PluginSettingsTab />}
|
||||
|
||||
@@ -20,6 +20,7 @@ const TAB_CONFIG: MainTabConfig[] = [
|
||||
{ id: 'git', labelKey: 'mainTabs.git', icon: GitBranch },
|
||||
{ id: 'api', labelKey: 'mainTabs.apiTokens', icon: Key },
|
||||
{ id: 'tasks', labelKey: 'mainTabs.tasks' },
|
||||
{ id: 'notifications', labelKey: 'mainTabs.notifications' },
|
||||
{ id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle },
|
||||
];
|
||||
|
||||
@@ -28,7 +29,7 @@ export default function SettingsMainTabs({ activeTab, onChange }: SettingsMainTa
|
||||
|
||||
return (
|
||||
<div className="border-b border-border">
|
||||
<div className="flex px-4 md:px-6" role="tablist" aria-label={t('mainTabs.label', { defaultValue: 'Settings' })}>
|
||||
<div className="flex px-4 md:px-6 overflow-x-auto scrollbar-hide" role="tablist" aria-label={t('mainTabs.label', { defaultValue: 'Settings' })}>
|
||||
{TAB_CONFIG.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.id;
|
||||
@@ -39,7 +40,7 @@ export default function SettingsMainTabs({ activeTab, onChange }: SettingsMainTa
|
||||
role="tab"
|
||||
aria-selected={isActive}
|
||||
onClick={() => onChange(tab.id)}
|
||||
className={`border-b-2 px-4 py-3 text-sm font-medium transition-colors ${
|
||||
className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
|
||||
isActive
|
||||
? 'border-blue-600 text-blue-600 dark:text-blue-400'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Bot, GitBranch, Key, ListChecks, Palette, Puzzle } from 'lucide-react';
|
||||
import { Bell, Bot, GitBranch, Key, ListChecks, Palette, Puzzle } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '../../../lib/utils';
|
||||
import { PillBar, Pill } from '../../../shared/view/ui';
|
||||
@@ -22,6 +22,7 @@ const NAV_ITEMS: NavItem[] = [
|
||||
{ id: 'api', labelKey: 'mainTabs.apiTokens', icon: Key },
|
||||
{ id: 'tasks', labelKey: 'mainTabs.tasks', icon: ListChecks },
|
||||
{ id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle },
|
||||
{ id: 'notifications', labelKey: 'mainTabs.notifications', icon: Bell },
|
||||
];
|
||||
|
||||
export default function SettingsSidebar({ activeTab, onChange }: SettingsSidebarProps) {
|
||||
|
||||
145
src/components/settings/view/tabs/NotificationsSettingsTab.tsx
Normal file
145
src/components/settings/view/tabs/NotificationsSettingsTab.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { Bell, BellOff, BellRing, Loader2 } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { NotificationPreferencesState } from '../../types/types';
|
||||
|
||||
type NotificationsSettingsTabProps = {
|
||||
notificationPreferences: NotificationPreferencesState;
|
||||
onNotificationPreferencesChange: (value: NotificationPreferencesState) => void;
|
||||
pushPermission: NotificationPermission | 'unsupported';
|
||||
isPushSubscribed: boolean;
|
||||
isPushLoading: boolean;
|
||||
onEnablePush: () => void;
|
||||
onDisablePush: () => void;
|
||||
};
|
||||
|
||||
export default function NotificationsSettingsTab({
|
||||
notificationPreferences,
|
||||
onNotificationPreferencesChange,
|
||||
pushPermission,
|
||||
isPushSubscribed,
|
||||
isPushLoading,
|
||||
onEnablePush,
|
||||
onDisablePush,
|
||||
}: NotificationsSettingsTabProps) {
|
||||
const { t } = useTranslation('settings');
|
||||
|
||||
const pushSupported = pushPermission !== 'unsupported';
|
||||
const pushDenied = pushPermission === 'denied';
|
||||
|
||||
return (
|
||||
<div className="space-y-6 md:space-y-8">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Bell className="w-5 h-5 text-blue-600" />
|
||||
<h3 className="text-lg font-medium text-foreground">{t('notifications.title')}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{t('notifications.description')}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 bg-card border border-border rounded-lg p-4">
|
||||
<h4 className="font-medium text-foreground">{t('notifications.webPush.title')}</h4>
|
||||
{!pushSupported ? (
|
||||
<p className="text-sm text-muted-foreground">{t('notifications.webPush.unsupported')}</p>
|
||||
) : pushDenied ? (
|
||||
<p className="text-sm text-muted-foreground">{t('notifications.webPush.denied')}</p>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isPushLoading}
|
||||
onClick={() => {
|
||||
if (isPushSubscribed) {
|
||||
onDisablePush();
|
||||
} else {
|
||||
onEnablePush();
|
||||
}
|
||||
}}
|
||||
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
|
||||
isPushSubscribed
|
||||
? 'bg-red-100 text-red-700 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600'
|
||||
}`}
|
||||
>
|
||||
{isPushLoading ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : isPushSubscribed ? (
|
||||
<BellOff className="w-4 h-4" />
|
||||
) : (
|
||||
<BellRing className="w-4 h-4" />
|
||||
)}
|
||||
{isPushLoading
|
||||
? t('notifications.webPush.loading')
|
||||
: isPushSubscribed
|
||||
? t('notifications.webPush.disable')
|
||||
: t('notifications.webPush.enable')}
|
||||
</button>
|
||||
{isPushSubscribed && (
|
||||
<span className="text-sm text-green-600 dark:text-green-400">
|
||||
{t('notifications.webPush.enabled')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 bg-card border border-border rounded-lg p-4">
|
||||
<h4 className="font-medium text-foreground">{t('notifications.events.title')}</h4>
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center gap-2 text-sm text-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={notificationPreferences.events.actionRequired}
|
||||
onChange={(event) =>
|
||||
onNotificationPreferencesChange({
|
||||
...notificationPreferences,
|
||||
events: {
|
||||
...notificationPreferences.events,
|
||||
actionRequired: event.target.checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
{t('notifications.events.actionRequired')}
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm text-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={notificationPreferences.events.stop}
|
||||
onChange={(event) =>
|
||||
onNotificationPreferencesChange({
|
||||
...notificationPreferences,
|
||||
events: {
|
||||
...notificationPreferences.events,
|
||||
stop: event.target.checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
{t('notifications.events.stop')}
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm text-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={notificationPreferences.events.error}
|
||||
onChange={(event) =>
|
||||
onNotificationPreferencesChange({
|
||||
...notificationPreferences,
|
||||
events: {
|
||||
...notificationPreferences.events,
|
||||
error: event.target.checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
{t('notifications.events.error')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -255,6 +255,7 @@ function ClaudePermissions({
|
||||
<li><code className="rounded bg-blue-100 px-1 dark:bg-blue-800">"Bash(rm:*)"</code> {t('permissions.toolExamples.bashRm')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user