mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
MAESTRO: Add configurable toast notification duration setting
Add user-configurable duration for toast notifications under Settings > Notifications. Users can select preset durations (5s, 10s, 20s, 30s) or "Never" which keeps toasts on screen until manually dismissed via the X button. Default is 20 seconds. When set to "Never", toasts stack without auto-dismissing and the progress bar is hidden.
This commit is contained in:
@@ -46,7 +46,7 @@ export default function MaestroConsole() {
|
||||
const { hasOpenLayers, hasOpenModal } = useLayerStack();
|
||||
|
||||
// --- TOAST NOTIFICATIONS ---
|
||||
const { addToast } = useToast();
|
||||
const { addToast, setDefaultDuration: setToastDefaultDuration } = useToast();
|
||||
|
||||
// --- SETTINGS (from useSettings hook) ---
|
||||
const settings = useSettings();
|
||||
@@ -73,6 +73,7 @@ export default function MaestroConsole() {
|
||||
osNotificationsEnabled, setOsNotificationsEnabled,
|
||||
audioFeedbackEnabled, setAudioFeedbackEnabled,
|
||||
audioFeedbackCommand, setAudioFeedbackCommand,
|
||||
toastDuration, setToastDuration,
|
||||
shortcuts, setShortcuts,
|
||||
customAICommands, setCustomAICommands,
|
||||
} = settings;
|
||||
@@ -187,6 +188,11 @@ export default function MaestroConsole() {
|
||||
}
|
||||
}, [logViewerOpen]);
|
||||
|
||||
// Sync toast duration setting to ToastContext
|
||||
useEffect(() => {
|
||||
setToastDefaultDuration(toastDuration);
|
||||
}, [toastDuration, setToastDefaultDuration]);
|
||||
|
||||
// Close file preview when switching sessions
|
||||
useEffect(() => {
|
||||
if (previewFile !== null) {
|
||||
@@ -3198,6 +3204,8 @@ export default function MaestroConsole() {
|
||||
setAudioFeedbackEnabled={setAudioFeedbackEnabled}
|
||||
audioFeedbackCommand={audioFeedbackCommand}
|
||||
setAudioFeedbackCommand={setAudioFeedbackCommand}
|
||||
toastDuration={toastDuration}
|
||||
setToastDuration={setToastDuration}
|
||||
customAICommands={customAICommands}
|
||||
setCustomAICommands={setCustomAICommands}
|
||||
initialTab={settingsTab}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { X, Key, Moon, Sun, Keyboard, Check, Terminal, Bell, Volume2, Cpu } from 'lucide-react';
|
||||
import { X, Key, Moon, Sun, Keyboard, Check, Terminal, Bell, Volume2, Cpu, Clock } from 'lucide-react';
|
||||
import type { AgentConfig, Theme, Shortcut, ShellInfo, CustomAICommand } from '../types';
|
||||
import { useLayerStack } from '../contexts/LayerStackContext';
|
||||
import { MODAL_PRIORITIES } from '../constants/modalPriorities';
|
||||
@@ -55,6 +55,8 @@ interface SettingsModalProps {
|
||||
setAudioFeedbackEnabled: (value: boolean) => void;
|
||||
audioFeedbackCommand: string;
|
||||
setAudioFeedbackCommand: (value: string) => void;
|
||||
toastDuration: number;
|
||||
setToastDuration: (value: number) => void;
|
||||
customAICommands: CustomAICommand[];
|
||||
setCustomAICommands: (commands: CustomAICommand[]) => void;
|
||||
initialTab?: 'general' | 'llm' | 'shortcuts' | 'theme' | 'network' | 'notifications' | 'aicommands';
|
||||
@@ -1445,6 +1447,79 @@ export function SettingsModal(props: SettingsModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toast Duration */}
|
||||
<div>
|
||||
<label className="block text-xs font-bold opacity-70 uppercase mb-2 flex items-center gap-2">
|
||||
<Clock className="w-3 h-3" />
|
||||
Toast Notification Duration
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => props.setToastDuration(5)}
|
||||
className={`flex-1 py-2 px-3 rounded border transition-all ${props.toastDuration === 5 ? 'ring-2' : ''}`}
|
||||
style={{
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: props.toastDuration === 5 ? theme.colors.accentDim : 'transparent',
|
||||
ringColor: theme.colors.accent,
|
||||
color: theme.colors.textMain
|
||||
}}
|
||||
>
|
||||
5s
|
||||
</button>
|
||||
<button
|
||||
onClick={() => props.setToastDuration(10)}
|
||||
className={`flex-1 py-2 px-3 rounded border transition-all ${props.toastDuration === 10 ? 'ring-2' : ''}`}
|
||||
style={{
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: props.toastDuration === 10 ? theme.colors.accentDim : 'transparent',
|
||||
ringColor: theme.colors.accent,
|
||||
color: theme.colors.textMain
|
||||
}}
|
||||
>
|
||||
10s
|
||||
</button>
|
||||
<button
|
||||
onClick={() => props.setToastDuration(20)}
|
||||
className={`flex-1 py-2 px-3 rounded border transition-all ${props.toastDuration === 20 ? 'ring-2' : ''}`}
|
||||
style={{
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: props.toastDuration === 20 ? theme.colors.accentDim : 'transparent',
|
||||
ringColor: theme.colors.accent,
|
||||
color: theme.colors.textMain
|
||||
}}
|
||||
>
|
||||
20s
|
||||
</button>
|
||||
<button
|
||||
onClick={() => props.setToastDuration(30)}
|
||||
className={`flex-1 py-2 px-3 rounded border transition-all ${props.toastDuration === 30 ? 'ring-2' : ''}`}
|
||||
style={{
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: props.toastDuration === 30 ? theme.colors.accentDim : 'transparent',
|
||||
ringColor: theme.colors.accent,
|
||||
color: theme.colors.textMain
|
||||
}}
|
||||
>
|
||||
30s
|
||||
</button>
|
||||
<button
|
||||
onClick={() => props.setToastDuration(0)}
|
||||
className={`flex-1 py-2 px-3 rounded border transition-all ${props.toastDuration === 0 ? 'ring-2' : ''}`}
|
||||
style={{
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: props.toastDuration === 0 ? theme.colors.accentDim : 'transparent',
|
||||
ringColor: theme.colors.accent,
|
||||
color: theme.colors.textMain
|
||||
}}
|
||||
>
|
||||
Never
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs opacity-50 mt-2">
|
||||
How long toast notifications remain on screen. "Never" means they stay until manually dismissed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Info about when notifications are triggered */}
|
||||
<div className="p-3 rounded-lg" style={{ backgroundColor: theme.colors.bgActivity, border: `1px solid ${theme.colors.border}` }}>
|
||||
<div className="text-xs font-medium mb-2" style={{ color: theme.colors.textMain }}>When are notifications triggered?</div>
|
||||
|
||||
@@ -17,32 +17,45 @@ interface ToastContextType {
|
||||
addToast: (toast: Omit<Toast, 'id' | 'timestamp'>) => void;
|
||||
removeToast: (id: string) => void;
|
||||
clearToasts: () => void;
|
||||
defaultDuration: number;
|
||||
setDefaultDuration: (duration: number) => void;
|
||||
}
|
||||
|
||||
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
||||
|
||||
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
||||
interface ToastProviderProps {
|
||||
children: React.ReactNode;
|
||||
defaultDuration?: number; // Duration in seconds, 0 = never auto-dismiss
|
||||
}
|
||||
|
||||
export function ToastProvider({ children, defaultDuration: initialDuration = 20 }: ToastProviderProps) {
|
||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||
const [defaultDuration, setDefaultDuration] = useState(initialDuration);
|
||||
const toastIdCounter = useRef(0);
|
||||
|
||||
const addToast = useCallback((toast: Omit<Toast, 'id' | 'timestamp'>) => {
|
||||
const id = `toast-${Date.now()}-${toastIdCounter.current++}`;
|
||||
// Convert seconds to ms, use 0 for "never dismiss"
|
||||
const durationMs = toast.duration !== undefined
|
||||
? toast.duration
|
||||
: (defaultDuration > 0 ? defaultDuration * 1000 : 0);
|
||||
|
||||
const newToast: Toast = {
|
||||
...toast,
|
||||
id,
|
||||
timestamp: Date.now(),
|
||||
duration: toast.duration ?? 5000, // Default 5 seconds
|
||||
duration: durationMs,
|
||||
};
|
||||
|
||||
setToasts(prev => [...prev, newToast]);
|
||||
|
||||
// Auto-remove after duration
|
||||
if (newToast.duration && newToast.duration > 0) {
|
||||
// Auto-remove after duration (only if duration > 0)
|
||||
if (durationMs > 0) {
|
||||
setTimeout(() => {
|
||||
setToasts(prev => prev.filter(t => t.id !== id));
|
||||
}, newToast.duration);
|
||||
}, durationMs);
|
||||
}
|
||||
}, []);
|
||||
}, [defaultDuration]);
|
||||
|
||||
const removeToast = useCallback((id: string) => {
|
||||
setToasts(prev => prev.filter(t => t.id !== id));
|
||||
@@ -53,7 +66,7 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ toasts, addToast, removeToast, clearToasts }}>
|
||||
<ToastContext.Provider value={{ toasts, addToast, removeToast, clearToasts, defaultDuration, setDefaultDuration }}>
|
||||
{children}
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
|
||||
@@ -79,6 +79,8 @@ export interface UseSettingsReturn {
|
||||
setAudioFeedbackEnabled: (value: boolean) => void;
|
||||
audioFeedbackCommand: string;
|
||||
setAudioFeedbackCommand: (value: string) => void;
|
||||
toastDuration: number;
|
||||
setToastDuration: (value: number) => void;
|
||||
|
||||
// Shortcuts
|
||||
shortcuts: Record<string, Shortcut>;
|
||||
@@ -132,6 +134,7 @@ export function useSettings(): UseSettingsReturn {
|
||||
const [osNotificationsEnabled, setOsNotificationsEnabledState] = useState(true); // Default: on
|
||||
const [audioFeedbackEnabled, setAudioFeedbackEnabledState] = useState(false); // Default: off
|
||||
const [audioFeedbackCommand, setAudioFeedbackCommandState] = useState('say'); // Default: macOS say command
|
||||
const [toastDuration, setToastDurationState] = useState(20); // Default: 20 seconds, 0 = never auto-dismiss
|
||||
|
||||
// Shortcuts
|
||||
const [shortcuts, setShortcutsState] = useState<Record<string, Shortcut>>(DEFAULT_SHORTCUTS);
|
||||
@@ -260,6 +263,11 @@ export function useSettings(): UseSettingsReturn {
|
||||
window.maestro.settings.set('audioFeedbackCommand', value);
|
||||
};
|
||||
|
||||
const setToastDuration = (value: number) => {
|
||||
setToastDurationState(value);
|
||||
window.maestro.settings.set('toastDuration', value);
|
||||
};
|
||||
|
||||
const setCustomAICommands = (value: CustomAICommand[]) => {
|
||||
setCustomAICommandsState(value);
|
||||
window.maestro.settings.set('customAICommands', value);
|
||||
@@ -295,6 +303,7 @@ export function useSettings(): UseSettingsReturn {
|
||||
const savedOsNotificationsEnabled = await window.maestro.settings.get('osNotificationsEnabled');
|
||||
const savedAudioFeedbackEnabled = await window.maestro.settings.get('audioFeedbackEnabled');
|
||||
const savedAudioFeedbackCommand = await window.maestro.settings.get('audioFeedbackCommand');
|
||||
const savedToastDuration = await window.maestro.settings.get('toastDuration');
|
||||
const savedCustomAICommands = await window.maestro.settings.get('customAICommands');
|
||||
|
||||
// Migration: if old setting exists but new ones don't, migrate
|
||||
@@ -329,6 +338,7 @@ export function useSettings(): UseSettingsReturn {
|
||||
if (savedOsNotificationsEnabled !== undefined) setOsNotificationsEnabledState(savedOsNotificationsEnabled);
|
||||
if (savedAudioFeedbackEnabled !== undefined) setAudioFeedbackEnabledState(savedAudioFeedbackEnabled);
|
||||
if (savedAudioFeedbackCommand !== undefined) setAudioFeedbackCommandState(savedAudioFeedbackCommand);
|
||||
if (savedToastDuration !== undefined) setToastDurationState(savedToastDuration);
|
||||
|
||||
// Merge saved shortcuts with defaults (in case new shortcuts were added)
|
||||
if (savedShortcuts !== undefined) {
|
||||
@@ -407,6 +417,8 @@ export function useSettings(): UseSettingsReturn {
|
||||
setAudioFeedbackEnabled,
|
||||
audioFeedbackCommand,
|
||||
setAudioFeedbackCommand,
|
||||
toastDuration,
|
||||
setToastDuration,
|
||||
shortcuts,
|
||||
setShortcuts,
|
||||
customAICommands,
|
||||
|
||||
Reference in New Issue
Block a user