- Introduced a new notification setting for task comments, allowing users to receive OS notifications when comments are added to tasks. - Updated relevant interfaces and configuration files to include the new `notifyOnTaskComments` option. - Enhanced notification handling logic to detect and notify users of new task comments, excluding comments made by the user themselves. - Updated UI components to support the new notification setting, ensuring a seamless user experience.
419 lines
13 KiB
TypeScript
419 lines
13 KiB
TypeScript
/**
|
|
* useSettingsHandlers - Hook for all settings action handlers.
|
|
* Groups handlers by section for better organization.
|
|
*/
|
|
|
|
import { useCallback, useRef } from 'react';
|
|
|
|
import { api } from '@renderer/api';
|
|
import { useStore } from '@renderer/store';
|
|
|
|
import type { RepositoryDropdownItem } from './useSettingsConfig';
|
|
import type { AppConfig, NotificationTrigger } from '@renderer/types/data';
|
|
|
|
// Get the setState function from the store to update appConfig globally
|
|
const setStoreState = useStore.setState;
|
|
|
|
interface UseSettingsHandlersProps {
|
|
config: AppConfig | null;
|
|
setSaving: (saving: boolean) => void;
|
|
setError: (error: string | null) => void;
|
|
setConfig: (config: AppConfig | null) => void;
|
|
setOptimisticConfig: React.Dispatch<React.SetStateAction<AppConfig | null>>;
|
|
updateConfig: (
|
|
section: keyof AppConfig,
|
|
data: Partial<AppConfig[keyof AppConfig]>
|
|
) => Promise<void>;
|
|
}
|
|
|
|
interface SettingsHandlers {
|
|
// General handlers
|
|
handleGeneralToggle: (key: keyof AppConfig['general'], value: boolean) => void;
|
|
handleThemeChange: (value: 'dark' | 'light' | 'system') => void;
|
|
handleLanguageChange: (value: string) => void;
|
|
handleDefaultTabChange: (value: 'dashboard' | 'last-session') => void;
|
|
|
|
// Notification handlers
|
|
handleNotificationToggle: (key: keyof AppConfig['notifications'], value: boolean) => void;
|
|
handleStatusChangeStatusesUpdate: (statuses: string[]) => void;
|
|
handleSnooze: (minutes: number) => Promise<void>;
|
|
handleClearSnooze: () => Promise<void>;
|
|
handleAddIgnoredRepository: (item: RepositoryDropdownItem) => Promise<void>;
|
|
handleRemoveIgnoredRepository: (repositoryId: string) => Promise<void>;
|
|
|
|
// Trigger handlers
|
|
handleAddTrigger: (trigger: Omit<NotificationTrigger, 'isBuiltin'>) => Promise<void>;
|
|
handleUpdateTrigger: (triggerId: string, updates: Partial<NotificationTrigger>) => Promise<void>;
|
|
handleRemoveTrigger: (triggerId: string) => Promise<void>;
|
|
|
|
// Display handlers
|
|
handleDisplayToggle: (key: keyof AppConfig['display'], value: boolean) => void;
|
|
|
|
// Advanced handlers
|
|
handleResetToDefaults: () => Promise<void>;
|
|
handleExportConfig: () => void;
|
|
handleImportConfig: () => void;
|
|
handleOpenInEditor: () => Promise<void>;
|
|
}
|
|
|
|
export function useSettingsHandlers({
|
|
config,
|
|
setSaving,
|
|
setError,
|
|
setConfig,
|
|
setOptimisticConfig,
|
|
updateConfig,
|
|
}: UseSettingsHandlersProps): SettingsHandlers {
|
|
// Use ref for config to avoid recreating callbacks when config changes
|
|
const configRef = useRef(config);
|
|
configRef.current = config;
|
|
|
|
// General handlers
|
|
const handleGeneralToggle = useCallback(
|
|
(key: keyof AppConfig['general'], value: boolean) => {
|
|
void updateConfig('general', { [key]: value });
|
|
},
|
|
[updateConfig]
|
|
);
|
|
|
|
const handleThemeChange = useCallback(
|
|
(value: 'dark' | 'light' | 'system') => {
|
|
void updateConfig('general', { theme: value });
|
|
},
|
|
[updateConfig]
|
|
);
|
|
|
|
const handleLanguageChange = useCallback(
|
|
(value: string) => {
|
|
void updateConfig('general', { agentLanguage: value });
|
|
},
|
|
[updateConfig]
|
|
);
|
|
|
|
const handleDefaultTabChange = useCallback(
|
|
(value: 'dashboard' | 'last-session') => {
|
|
void updateConfig('general', { defaultTab: value });
|
|
},
|
|
[updateConfig]
|
|
);
|
|
|
|
// Notification handlers
|
|
const handleNotificationToggle = useCallback(
|
|
(key: keyof AppConfig['notifications'], value: boolean) => {
|
|
void updateConfig('notifications', { [key]: value });
|
|
},
|
|
[updateConfig]
|
|
);
|
|
|
|
const handleStatusChangeStatusesUpdate = useCallback(
|
|
(statuses: string[]) => {
|
|
void updateConfig('notifications', { statusChangeStatuses: statuses });
|
|
},
|
|
[updateConfig]
|
|
);
|
|
|
|
const handleSnooze = useCallback(
|
|
async (minutes: number) => {
|
|
try {
|
|
setSaving(true);
|
|
const updatedConfig = await api.config.snooze(minutes);
|
|
setConfig(updatedConfig);
|
|
setOptimisticConfig(updatedConfig);
|
|
setStoreState({ appConfig: updatedConfig });
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to snooze notifications');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
},
|
|
[setSaving, setConfig, setOptimisticConfig, setError]
|
|
);
|
|
|
|
const handleClearSnooze = useCallback(async () => {
|
|
try {
|
|
setSaving(true);
|
|
const updatedConfig = await api.config.clearSnooze();
|
|
setConfig(updatedConfig);
|
|
setOptimisticConfig(updatedConfig);
|
|
setStoreState({ appConfig: updatedConfig });
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to clear snooze');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}, [setSaving, setConfig, setOptimisticConfig, setError]);
|
|
|
|
const handleAddIgnoredRepository = useCallback(
|
|
async (item: RepositoryDropdownItem) => {
|
|
try {
|
|
setSaving(true);
|
|
const updatedConfig = await api.config.addIgnoreRepository(item.id);
|
|
setConfig(updatedConfig);
|
|
setOptimisticConfig(updatedConfig);
|
|
setStoreState({ appConfig: updatedConfig });
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to add repository');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
},
|
|
[setSaving, setConfig, setOptimisticConfig, setError]
|
|
);
|
|
|
|
const handleRemoveIgnoredRepository = useCallback(
|
|
async (repositoryId: string) => {
|
|
try {
|
|
setSaving(true);
|
|
const updatedConfig = await api.config.removeIgnoreRepository(repositoryId);
|
|
setConfig(updatedConfig);
|
|
setOptimisticConfig(updatedConfig);
|
|
setStoreState({ appConfig: updatedConfig });
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to remove repository');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
},
|
|
[setSaving, setConfig, setOptimisticConfig, setError]
|
|
);
|
|
|
|
// Trigger handlers
|
|
const handleAddTrigger = useCallback(
|
|
async (trigger: Omit<NotificationTrigger, 'isBuiltin'>) => {
|
|
try {
|
|
setSaving(true);
|
|
const updatedConfig = await api.config.addTrigger(trigger);
|
|
setConfig(updatedConfig);
|
|
setOptimisticConfig(updatedConfig);
|
|
setStoreState({ appConfig: updatedConfig });
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to add trigger');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
},
|
|
[setSaving, setConfig, setOptimisticConfig, setError]
|
|
);
|
|
|
|
const handleUpdateTrigger = useCallback(
|
|
async (triggerId: string, updates: Partial<NotificationTrigger>) => {
|
|
// Optimistic update - immediately reflect the change in UI
|
|
setOptimisticConfig((prev) => {
|
|
if (!prev) return prev;
|
|
const updatedTriggers =
|
|
prev.notifications.triggers?.map((t) =>
|
|
t.id === triggerId ? { ...t, ...updates } : t
|
|
) ?? [];
|
|
return {
|
|
...prev,
|
|
notifications: {
|
|
...prev.notifications,
|
|
triggers: updatedTriggers,
|
|
},
|
|
};
|
|
});
|
|
|
|
try {
|
|
setSaving(true);
|
|
const updatedConfig = await api.config.updateTrigger(triggerId, updates);
|
|
setConfig(updatedConfig);
|
|
setOptimisticConfig(updatedConfig);
|
|
setStoreState({ appConfig: updatedConfig });
|
|
} catch (err) {
|
|
// Revert optimistic update on error using ref to avoid stale closure
|
|
setOptimisticConfig(configRef.current);
|
|
setError(err instanceof Error ? err.message : 'Failed to update trigger');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
},
|
|
[setSaving, setConfig, setOptimisticConfig, setError]
|
|
);
|
|
|
|
const handleRemoveTrigger = useCallback(
|
|
async (triggerId: string) => {
|
|
try {
|
|
setSaving(true);
|
|
const updatedConfig = await api.config.removeTrigger(triggerId);
|
|
setConfig(updatedConfig);
|
|
setOptimisticConfig(updatedConfig);
|
|
setStoreState({ appConfig: updatedConfig });
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to remove trigger');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
},
|
|
[setSaving, setConfig, setOptimisticConfig, setError]
|
|
);
|
|
|
|
// Display handlers
|
|
const handleDisplayToggle = useCallback(
|
|
(key: keyof AppConfig['display'], value: boolean) => {
|
|
void updateConfig('display', { [key]: value });
|
|
},
|
|
[updateConfig]
|
|
);
|
|
|
|
// Advanced handlers
|
|
const handleResetToDefaults = useCallback(async () => {
|
|
if (!confirm('Are you sure you want to reset all settings to defaults?')) {
|
|
return;
|
|
}
|
|
try {
|
|
setSaving(true);
|
|
const defaultIgnoredRegex = ["The user doesn't want to proceed with this tool use\\."];
|
|
const defaultTriggers: NotificationTrigger[] = [
|
|
{
|
|
id: 'builtin-tool-result-error',
|
|
name: 'Tool Result Error',
|
|
enabled: true,
|
|
contentType: 'tool_result',
|
|
mode: 'error_status',
|
|
requireError: true,
|
|
ignorePatterns: ["The user doesn't want to proceed with this tool use\\."],
|
|
isBuiltin: true,
|
|
},
|
|
{
|
|
id: 'builtin-bash-command',
|
|
name: 'Bash Command Alert for .env files',
|
|
enabled: true,
|
|
contentType: 'tool_use',
|
|
toolName: 'Bash',
|
|
mode: 'content_match',
|
|
matchField: 'command',
|
|
matchPattern: '/.env',
|
|
isBuiltin: true,
|
|
},
|
|
];
|
|
const defaultConfig: AppConfig = {
|
|
notifications: {
|
|
enabled: true,
|
|
soundEnabled: true,
|
|
ignoredRegex: defaultIgnoredRegex,
|
|
ignoredRepositories: [],
|
|
snoozedUntil: null,
|
|
snoozeMinutes: 30,
|
|
includeSubagentErrors: false,
|
|
notifyOnLeadInbox: false,
|
|
notifyOnUserInbox: true,
|
|
notifyOnClarifications: true,
|
|
notifyOnStatusChange: true,
|
|
notifyOnTaskComments: true,
|
|
statusChangeOnlySolo: true,
|
|
statusChangeStatuses: ['in_progress', 'completed'],
|
|
triggers: defaultTriggers,
|
|
},
|
|
general: {
|
|
launchAtLogin: false,
|
|
showDockIcon: true,
|
|
theme: 'dark',
|
|
defaultTab: 'dashboard',
|
|
claudeRootPath: null,
|
|
agentLanguage: 'system',
|
|
autoExpandAIGroups: false,
|
|
useNativeTitleBar: false,
|
|
},
|
|
display: {
|
|
showTimestamps: true,
|
|
compactMode: false,
|
|
syntaxHighlighting: true,
|
|
},
|
|
sessions: {
|
|
pinnedSessions: {},
|
|
hiddenSessions: {},
|
|
},
|
|
};
|
|
|
|
await api.config.update('notifications', defaultConfig.notifications);
|
|
await api.config.update('general', defaultConfig.general);
|
|
const updatedConfig = await api.config.update('display', defaultConfig.display);
|
|
setConfig(updatedConfig);
|
|
setOptimisticConfig(updatedConfig);
|
|
setStoreState({ appConfig: updatedConfig });
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to reset settings');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}, [setSaving, setConfig, setOptimisticConfig, setError]);
|
|
|
|
const handleExportConfig = useCallback(() => {
|
|
if (!configRef.current) return;
|
|
const dataStr = JSON.stringify(configRef.current, null, 2);
|
|
const blob = new Blob([dataStr], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = 'claude-devtools-config.json';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
}, []);
|
|
|
|
const handleOpenInEditor = useCallback(async () => {
|
|
try {
|
|
await api.config.openInEditor();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to open config in editor');
|
|
}
|
|
}, [setError]);
|
|
|
|
const handleImportConfig = useCallback(() => {
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = '.json';
|
|
input.onchange = async (e) => {
|
|
const file = (e.target as HTMLInputElement).files?.[0];
|
|
if (!file) return;
|
|
|
|
try {
|
|
setSaving(true);
|
|
const text = await file.text();
|
|
const importedConfig = JSON.parse(text) as AppConfig;
|
|
|
|
if (importedConfig.notifications) {
|
|
await api.config.update('notifications', importedConfig.notifications);
|
|
}
|
|
if (importedConfig.general) {
|
|
await api.config.update('general', importedConfig.general);
|
|
}
|
|
if (importedConfig.display) {
|
|
await api.config.update('display', importedConfig.display);
|
|
}
|
|
|
|
const updatedConfig = await api.config.get();
|
|
setConfig(updatedConfig);
|
|
setOptimisticConfig(updatedConfig);
|
|
setStoreState({ appConfig: updatedConfig });
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to import config');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
input.click();
|
|
}, [setSaving, setConfig, setOptimisticConfig, setError]);
|
|
|
|
return {
|
|
handleGeneralToggle,
|
|
handleThemeChange,
|
|
handleLanguageChange,
|
|
handleDefaultTabChange,
|
|
handleNotificationToggle,
|
|
handleStatusChangeStatusesUpdate,
|
|
handleSnooze,
|
|
handleClearSnooze,
|
|
handleAddIgnoredRepository,
|
|
handleRemoveIgnoredRepository,
|
|
handleAddTrigger,
|
|
handleUpdateTrigger,
|
|
handleRemoveTrigger,
|
|
handleDisplayToggle,
|
|
handleResetToDefaults,
|
|
handleExportConfig,
|
|
handleImportConfig,
|
|
handleOpenInEditor,
|
|
};
|
|
}
|