From a5767adf5b374bc194a29a5a38946cfd79182b93 Mon Sep 17 00:00:00 2001 From: Pedram Amini Date: Wed, 26 Nov 2025 16:36:15 -0600 Subject: [PATCH] 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. --- src/renderer/App.tsx | 10 ++- src/renderer/components/SettingsModal.tsx | 77 ++++++++++++++++++++++- src/renderer/contexts/ToastContext.tsx | 27 +++++--- src/renderer/hooks/useSettings.ts | 12 ++++ 4 files changed, 117 insertions(+), 9 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 80fb1e1f..1798417d 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -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} diff --git a/src/renderer/components/SettingsModal.tsx b/src/renderer/components/SettingsModal.tsx index c6a6436a..08c34e10 100644 --- a/src/renderer/components/SettingsModal.tsx +++ b/src/renderer/components/SettingsModal.tsx @@ -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) { + {/* Toast Duration */} +
+ +
+ + + + + +
+

+ How long toast notifications remain on screen. "Never" means they stay until manually dismissed. +

+
+ {/* Info about when notifications are triggered */}
When are notifications triggered?
diff --git a/src/renderer/contexts/ToastContext.tsx b/src/renderer/contexts/ToastContext.tsx index feed492d..685e2057 100644 --- a/src/renderer/contexts/ToastContext.tsx +++ b/src/renderer/contexts/ToastContext.tsx @@ -17,32 +17,45 @@ interface ToastContextType { addToast: (toast: Omit) => void; removeToast: (id: string) => void; clearToasts: () => void; + defaultDuration: number; + setDefaultDuration: (duration: number) => void; } const ToastContext = createContext(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([]); + const [defaultDuration, setDefaultDuration] = useState(initialDuration); const toastIdCounter = useRef(0); const addToast = useCallback((toast: Omit) => { 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 ( - + {children} ); diff --git a/src/renderer/hooks/useSettings.ts b/src/renderer/hooks/useSettings.ts index 2b628943..30761191 100644 --- a/src/renderer/hooks/useSettings.ts +++ b/src/renderer/hooks/useSettings.ts @@ -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; @@ -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>(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,