# Global Notification System — Sonner Toast Integration ## Context The adiuvAI Electron app has **52+ user-facing mutations** (create/update/delete for tasks, projects, clients, notes, timeline events, agents, settings, auth) with **no unified feedback system**. Some components show a transient "Saved" button label for 2s via `useState` + `setTimeout`; most mutations are completely silent. Errors are handled inconsistently — some show inline text, many are swallowed. This plan adds a global toast notification system using **shadcn's sonner component**, replacing all ad-hoc patterns with a single i18n-aware API. --- ## Phase 1: Foundation ### 1.1 Install sonner via shadcn CLI ```bash cd adiuvAI && npx shadcn@latest add sonner ``` This installs the `sonner` npm package and generates `src/renderer/components/ui/sonner.tsx`. ### 1.2 Fix theme import in generated `sonner.tsx` The generated file imports `useTheme` from `next-themes` (doesn't exist in this app). Replace with: ```tsx import { useTheme } from "@/components/theme-provider" ``` The app's `useTheme()` returns `{ theme: "dark" | "light" | "system" }` — same shape sonner expects. Configure `position="bottom-right"` to avoid sidebar collision. Keep `richColors` enabled for variant-specific coloring. Full target file: ```tsx import { useTheme } from "@/components/theme-provider" import { Toaster as Sonner } from "sonner" type ToasterProps = React.ComponentProps const Toaster = ({ ...props }: ToasterProps) => { const { theme = "system" } = useTheme() return ( ) } export { Toaster } ``` ### 1.3 Place `` in `src/renderer/index.tsx` Add `` as a sibling of `` inside ``: ```tsx import { Toaster } from '@/components/ui/sonner'; // ... ``` **Why here?** The `` must render OUTSIDE all conditional rendering in AppShell.tsx (which gates LoginForm / OnboardingFlow / main app). Placing it in `index.tsx` ensures toasts work in all three states. ### 1.4 Create `useNotify()` hook **New file:** `src/renderer/hooks/useNotify.ts` ```tsx import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; type ToastVariant = 'success' | 'error' | 'info' | 'warning'; interface NotifyOptions { descriptionKey?: string; values?: Record; duration?: number; } export function useNotify() { const { t } = useTranslation(); function notify(variant: ToastVariant, messageKey: string, options?: NotifyOptions) { const message = t(messageKey, options?.values); const description = options?.descriptionKey ? t(options.descriptionKey, options?.values) : undefined; const duration = options?.duration; switch (variant) { case 'success': toast.success(message, { description, duration: duration ?? 3000 }); break; case 'error': toast.error(message, { description, duration: duration ?? Infinity }); break; case 'info': toast.info(message, { description, duration: duration ?? 3000 }); break; case 'warning': toast.warning(message, { description, duration: duration ?? 4000 }); break; } } function notifyError(messageKey: string, error?: { message?: string }) { toast.error(t(messageKey), { description: error?.message, duration: Infinity }); } function notifyPromise(promise: Promise, keys: { loading: string; success: string; error: string }) { toast.promise(promise, { loading: t(keys.loading), success: t(keys.success), error: t(keys.error) }); } return { notify, notifyError, notifyPromise }; } ``` **Design rationale:** - Error toasts: `duration: Infinity` — persist until dismissed so users can read/copy errors - Success: 3s auto-dismiss — brief confirmation - Warning (destructive): 4s — slightly longer for delete confirmations - `notifyError`: convenience for `onError` callbacks — title from i18n, description from raw error - `notifyPromise`: wraps `toast.promise()` for long-running ops - All text goes through `t()` for i18n ### 1.5 Add i18n toast keys **Files:** `src/renderer/locales/{en,it,es,fr,de}/translation.json` Add a `toast` top-level key. English: ```json "toast": { "profile": { "updated": "Profile updated", "updateError": "Failed to update profile" }, "settings": { "languageChanged": "Language changed", "backendUrlSaved": "Server URL saved", "backendUrlError": "Failed to save server URL", "formatPrefsSaved": "Display preferences saved", "formatPrefsError": "Failed to save display preferences", "memorySaved": "Preferences saved", "memoryError": "Failed to save preferences" }, "auth": { "loginError": "Sign-in failed", "registerError": "Registration failed", "oauthError": "Google sign-in failed", "loggedOut": "Signed out" }, "onboarding": { "completed": "Onboarding complete", "completedDescription": "Your workspace is personalized", "error": "Failed to save onboarding", "reset": "Onboarding reset", "normalizing": "Personalizing your workspace...", "normalized": "Personalization ready" }, "task": { "created": "Task created", "createError": "Failed to create task", "updated": "Task updated", "updateError": "Failed to update task", "deleted": "Task deleted", "deleteError": "Failed to delete task" }, "project": { "created": "Project created", "createError": "Failed to create project", "updated": "Project updated", "updateError": "Failed to update project", "deleted": "Project deleted", "deleteError": "Failed to delete project", "archived": "Project archived", "unarchived": "Project unarchived", "archivedAll": "All projects archived", "unarchivedAll": "All projects unarchived" }, "client": { "created": "Client created", "createError": "Failed to create client", "updated": "Client renamed", "updateError": "Failed to rename client", "deleted": "Client deleted", "deleteError": "Failed to delete client" }, "note": { "created": "Note created", "createError": "Failed to create note", "deleted": "Note deleted", "deleteError": "Failed to delete note" }, "timeline": { "created": "Event created", "createError": "Failed to create event", "updated": "Event updated", "updateError": "Failed to update event", "deleted": "Event deleted", "deleteError": "Failed to delete event" }, "comment": { "created": "Comment added", "createError": "Failed to add comment", "deleted": "Comment deleted", "deleteError": "Failed to delete comment" }, "agent": { "created": "Agent created", "createError": "Failed to create agent", "updated": "Agent configuration saved", "updateError": "Failed to save agent configuration", "deleted": "Agent deleted", "deleteError": "Failed to delete agent", "runStarted": "Agent run started", "runError": "Failed to start agent" } } ``` **Italian translations:** ```json "toast": { "profile": { "updated": "Profilo aggiornato", "updateError": "Impossibile aggiornare il profilo" }, "settings": { "languageChanged": "Lingua cambiata", "backendUrlSaved": "URL del server salvato", "backendUrlError": "Impossibile salvare l'URL del server", "formatPrefsSaved": "Preferenze di visualizzazione salvate", "formatPrefsError": "Impossibile salvare le preferenze", "memorySaved": "Preferenze salvate", "memoryError": "Impossibile salvare le preferenze" }, "auth": { "loginError": "Accesso fallito", "registerError": "Registrazione fallita", "oauthError": "Accesso con Google fallito", "loggedOut": "Disconnesso" }, "onboarding": { "completed": "Onboarding completato", "completedDescription": "Il tuo workspace e' personalizzato", "error": "Impossibile salvare l'onboarding", "reset": "Onboarding ripristinato", "normalizing": "Personalizzazione in corso...", "normalized": "Personalizzazione pronta" }, "task": { "created": "Attivita' creata", "createError": "Impossibile creare l'attivita'", "updated": "Attivita' aggiornata", "updateError": "Impossibile aggiornare l'attivita'", "deleted": "Attivita' eliminata", "deleteError": "Impossibile eliminare l'attivita'" }, "project": { "created": "Progetto creato", "createError": "Impossibile creare il progetto", "updated": "Progetto aggiornato", "updateError": "Impossibile aggiornare il progetto", "deleted": "Progetto eliminato", "deleteError": "Impossibile eliminare il progetto", "archived": "Progetto archiviato", "unarchived": "Progetto ripristinato", "archivedAll": "Tutti i progetti archiviati", "unarchivedAll": "Tutti i progetti ripristinati" }, "client": { "created": "Cliente creato", "createError": "Impossibile creare il cliente", "updated": "Cliente rinominato", "updateError": "Impossibile rinominare il cliente", "deleted": "Cliente eliminato", "deleteError": "Impossibile eliminare il cliente" }, "note": { "created": "Nota creata", "createError": "Impossibile creare la nota", "deleted": "Nota eliminata", "deleteError": "Impossibile eliminare la nota" }, "timeline": { "created": "Evento creato", "createError": "Impossibile creare l'evento", "updated": "Evento aggiornato", "updateError": "Impossibile aggiornare l'evento", "deleted": "Evento eliminato", "deleteError": "Impossibile eliminare l'evento" }, "comment": { "created": "Commento aggiunto", "createError": "Impossibile aggiungere il commento", "deleted": "Commento eliminato", "deleteError": "Impossibile eliminare il commento" }, "agent": { "created": "Agente creato", "createError": "Impossibile creare l'agente", "updated": "Configurazione agente salvata", "updateError": "Impossibile salvare la configurazione", "deleted": "Agente eliminato", "deleteError": "Impossibile eliminare l'agente", "runStarted": "Esecuzione agente avviata", "runError": "Impossibile avviare l'agente" } } ``` **Spanish translations:** ```json "toast": { "profile": { "updated": "Perfil actualizado", "updateError": "Error al actualizar el perfil" }, "settings": { "languageChanged": "Idioma cambiado", "backendUrlSaved": "URL del servidor guardada", "backendUrlError": "Error al guardar la URL del servidor", "formatPrefsSaved": "Preferencias de visualizacion guardadas", "formatPrefsError": "Error al guardar las preferencias", "memorySaved": "Preferencias guardadas", "memoryError": "Error al guardar las preferencias" }, "auth": { "loginError": "Error de acceso", "registerError": "Error de registro", "oauthError": "Error de acceso con Google", "loggedOut": "Sesion cerrada" }, "onboarding": { "completed": "Configuracion completada", "completedDescription": "Tu espacio de trabajo esta personalizado", "error": "Error al guardar la configuracion", "reset": "Configuracion reiniciada", "normalizing": "Personalizando tu espacio...", "normalized": "Personalizacion lista" }, "task": { "created": "Tarea creada", "createError": "Error al crear la tarea", "updated": "Tarea actualizada", "updateError": "Error al actualizar la tarea", "deleted": "Tarea eliminada", "deleteError": "Error al eliminar la tarea" }, "project": { "created": "Proyecto creado", "createError": "Error al crear el proyecto", "updated": "Proyecto actualizado", "updateError": "Error al actualizar el proyecto", "deleted": "Proyecto eliminado", "deleteError": "Error al eliminar el proyecto", "archived": "Proyecto archivado", "unarchived": "Proyecto restaurado", "archivedAll": "Todos los proyectos archivados", "unarchivedAll": "Todos los proyectos restaurados" }, "client": { "created": "Cliente creado", "createError": "Error al crear el cliente", "updated": "Cliente renombrado", "updateError": "Error al renombrar el cliente", "deleted": "Cliente eliminado", "deleteError": "Error al eliminar el cliente" }, "note": { "created": "Nota creada", "createError": "Error al crear la nota", "deleted": "Nota eliminada", "deleteError": "Error al eliminar la nota" }, "timeline": { "created": "Evento creado", "createError": "Error al crear el evento", "updated": "Evento actualizado", "updateError": "Error al actualizar el evento", "deleted": "Evento eliminado", "deleteError": "Error al eliminar el evento" }, "comment": { "created": "Comentario agregado", "createError": "Error al agregar el comentario", "deleted": "Comentario eliminado", "deleteError": "Error al eliminar el comentario" }, "agent": { "created": "Agente creado", "createError": "Error al crear el agente", "updated": "Configuracion del agente guardada", "updateError": "Error al guardar la configuracion", "deleted": "Agente eliminado", "deleteError": "Error al eliminar el agente", "runStarted": "Ejecucion del agente iniciada", "runError": "Error al iniciar el agente" } } ``` **French translations:** ```json "toast": { "profile": { "updated": "Profil mis a jour", "updateError": "Impossible de mettre a jour le profil" }, "settings": { "languageChanged": "Langue modifiee", "backendUrlSaved": "URL du serveur enregistree", "backendUrlError": "Impossible d'enregistrer l'URL du serveur", "formatPrefsSaved": "Preferences d'affichage enregistrees", "formatPrefsError": "Impossible d'enregistrer les preferences", "memorySaved": "Preferences enregistrees", "memoryError": "Impossible d'enregistrer les preferences" }, "auth": { "loginError": "Echec de la connexion", "registerError": "Echec de l'inscription", "oauthError": "Echec de la connexion Google", "loggedOut": "Deconnecte" }, "onboarding": { "completed": "Configuration terminee", "completedDescription": "Votre espace de travail est personnalise", "error": "Impossible d'enregistrer la configuration", "reset": "Configuration reinitialisee", "normalizing": "Personnalisation en cours...", "normalized": "Personnalisation terminee" }, "task": { "created": "Tache creee", "createError": "Impossible de creer la tache", "updated": "Tache mise a jour", "updateError": "Impossible de mettre a jour la tache", "deleted": "Tache supprimee", "deleteError": "Impossible de supprimer la tache" }, "project": { "created": "Projet cree", "createError": "Impossible de creer le projet", "updated": "Projet mis a jour", "updateError": "Impossible de mettre a jour le projet", "deleted": "Projet supprime", "deleteError": "Impossible de supprimer le projet", "archived": "Projet archive", "unarchived": "Projet restaure", "archivedAll": "Tous les projets archives", "unarchivedAll": "Tous les projets restaures" }, "client": { "created": "Client cree", "createError": "Impossible de creer le client", "updated": "Client renomme", "updateError": "Impossible de renommer le client", "deleted": "Client supprime", "deleteError": "Impossible de supprimer le client" }, "note": { "created": "Note creee", "createError": "Impossible de creer la note", "deleted": "Note supprimee", "deleteError": "Impossible de supprimer la note" }, "timeline": { "created": "Evenement cree", "createError": "Impossible de creer l'evenement", "updated": "Evenement mis a jour", "updateError": "Impossible de mettre a jour l'evenement", "deleted": "Evenement supprime", "deleteError": "Impossible de supprimer l'evenement" }, "comment": { "created": "Commentaire ajoute", "createError": "Impossible d'ajouter le commentaire", "deleted": "Commentaire supprime", "deleteError": "Impossible de supprimer le commentaire" }, "agent": { "created": "Agent cree", "createError": "Impossible de creer l'agent", "updated": "Configuration de l'agent enregistree", "updateError": "Impossible d'enregistrer la configuration", "deleted": "Agent supprime", "deleteError": "Impossible de supprimer l'agent", "runStarted": "Execution de l'agent lancee", "runError": "Impossible de lancer l'agent" } } ``` **German translations:** ```json "toast": { "profile": { "updated": "Profil aktualisiert", "updateError": "Profil konnte nicht aktualisiert werden" }, "settings": { "languageChanged": "Sprache geaendert", "backendUrlSaved": "Server-URL gespeichert", "backendUrlError": "Server-URL konnte nicht gespeichert werden", "formatPrefsSaved": "Anzeigeeinstellungen gespeichert", "formatPrefsError": "Einstellungen konnten nicht gespeichert werden", "memorySaved": "Einstellungen gespeichert", "memoryError": "Einstellungen konnten nicht gespeichert werden" }, "auth": { "loginError": "Anmeldung fehlgeschlagen", "registerError": "Registrierung fehlgeschlagen", "oauthError": "Google-Anmeldung fehlgeschlagen", "loggedOut": "Abgemeldet" }, "onboarding": { "completed": "Einrichtung abgeschlossen", "completedDescription": "Ihr Arbeitsbereich ist personalisiert", "error": "Einrichtung konnte nicht gespeichert werden", "reset": "Einrichtung zurueckgesetzt", "normalizing": "Personalisierung laeuft...", "normalized": "Personalisierung abgeschlossen" }, "task": { "created": "Aufgabe erstellt", "createError": "Aufgabe konnte nicht erstellt werden", "updated": "Aufgabe aktualisiert", "updateError": "Aufgabe konnte nicht aktualisiert werden", "deleted": "Aufgabe geloescht", "deleteError": "Aufgabe konnte nicht geloescht werden" }, "project": { "created": "Projekt erstellt", "createError": "Projekt konnte nicht erstellt werden", "updated": "Projekt aktualisiert", "updateError": "Projekt konnte nicht aktualisiert werden", "deleted": "Projekt geloescht", "deleteError": "Projekt konnte nicht geloescht werden", "archived": "Projekt archiviert", "unarchived": "Projekt wiederhergestellt", "archivedAll": "Alle Projekte archiviert", "unarchivedAll": "Alle Projekte wiederhergestellt" }, "client": { "created": "Kunde erstellt", "createError": "Kunde konnte nicht erstellt werden", "updated": "Kunde umbenannt", "updateError": "Kunde konnte nicht umbenannt werden", "deleted": "Kunde geloescht", "deleteError": "Kunde konnte nicht geloescht werden" }, "note": { "created": "Notiz erstellt", "createError": "Notiz konnte nicht erstellt werden", "deleted": "Notiz geloescht", "deleteError": "Notiz konnte nicht geloescht werden" }, "timeline": { "created": "Ereignis erstellt", "createError": "Ereignis konnte nicht erstellt werden", "updated": "Ereignis aktualisiert", "updateError": "Ereignis konnte nicht aktualisiert werden", "deleted": "Ereignis geloescht", "deleteError": "Ereignis konnte nicht geloescht werden" }, "comment": { "created": "Kommentar hinzugefuegt", "createError": "Kommentar konnte nicht hinzugefuegt werden", "deleted": "Kommentar geloescht", "deleteError": "Kommentar konnte nicht geloescht werden" }, "agent": { "created": "Agent erstellt", "createError": "Agent konnte nicht erstellt werden", "updated": "Agent-Konfiguration gespeichert", "updateError": "Konfiguration konnte nicht gespeichert werden", "deleted": "Agent geloescht", "deleteError": "Agent konnte nicht geloescht werden", "runStarted": "Agent-Ausfuehrung gestartet", "runError": "Agent konnte nicht gestartet werden" } } ``` --- ## Phase 2: Settings Mutations (replace existing `saved`/`setSaved` patterns) These 5 components have existing feedback to **remove and replace**: ### 2.1 `src/renderer/components/settings/GeneralSection.tsx` **Current:** `saved`/`setSaved` state (line 28), `error`/`setError` state (line 29), `setTimeout` (line 44), inline `

` error (line 93). **Changes:** 1. Add `const { notify, notifyError } = useNotify();` 2. **Remove** `const [saved, setSaved] = useState(false);` 3. **Remove** `const [error, setError] = useState('');` 4. In `handleSave` `onSuccess`: remove `setSaved(true); setTimeout(...)` → add `notify('success', 'toast.profile.updated');` 5. In `handleSave` `onError`: replace `setError(err.message)` → `notifyError('toast.profile.updateError', err);` 6. **Remove** inline error `

` tag (line 93) 7. **Remove** `setSaved(false)` from `onChange` handlers (lines 83, 89) 8. Button text: `{saved ? t('settings.saved') : t('common.save')}` → `{t('common.save')}` 9. In `handleLanguageChange`: add `notify('info', 'toast.settings.languageChanged');` ### 2.2 `src/renderer/components/settings/ProfileSection.tsx` **Current:** `profileSaved`/`displaySaved` states, both with `setTimeout`. **Changes:** 1. Add `useNotify()`, remove both `saved` states and `setTimeout`s 2. Profile save `onSuccess` → `notify('success', 'toast.settings.memorySaved')` 3. Display save `onSuccess` → `notify('success', 'toast.settings.formatPrefsSaved')` 4. Reset onboarding `onSuccess` → `notify('info', 'toast.onboarding.reset')` 5. Both save buttons: replace ternary with `{t('common.save')}` ### 2.3 `src/renderer/components/settings/AccountSection.tsx` **Current:** `urlSaved`/`setUrlSaved` state, `setTimeout`. **Changes:** 1. Add `useNotify()`, remove `urlSaved` state and `setTimeout` 2. Backend URL save `onSuccess` → `notify('success', 'toast.settings.backendUrlSaved')` 3. Add `onError` → `notifyError('toast.settings.backendUrlError', err)` 4. Logout `onSuccess` → `notify('info', 'toast.auth.loggedOut')` 5. Button text: replace ternary with `{t('common.save')}` ### 2.4 `src/renderer/components/settings/LocalAgentConfigPanel.tsx` **Current:** `saved`/`setSaved` state, `setTimeout`. **Changes:** 1. Add `useNotify()`, remove `saved` state and `setTimeout` 2. Save `onSuccess` → `notify('success', 'toast.agent.updated')` 3. Add `onError` → `notifyError('toast.agent.updateError', err)` 4. Button text: `{t('common.save')}` ### 2.5 `src/renderer/components/settings/CloudAgentConfigPanel.tsx` Identical pattern to 2.4. --- ## Phase 3: CRUD Operations ### Tasks | File | Mutation | Toast | Key | |------|----------|-------|-----| | `components/tasks/NewTaskDialog.tsx` | `tasks.create` | success | `toast.task.created` | | `components/tasks/EditTaskDialog.tsx` | `tasks.update` | success | `toast.task.updated` | | `components/tasks/TaskDetailDialog.tsx` | `taskComments.create` | success | `toast.comment.created` | | `components/tasks/TaskDetailDialog.tsx` | `taskComments.delete` | warning | `toast.comment.deleted` | | `routes/tasks.tsx` | `tasks.delete` | warning | `toast.task.deleted` | | `components/projects/KanbanBoard.tsx` | `tasks.delete` | warning | `toast.task.deleted` | | `components/projects/ProjectDetail.tsx` | `tasks.delete` | warning | `toast.task.deleted` | | `components/ai/blocks/ChatEntityBlock.tsx` | `tasks.delete` | warning | `toast.task.deleted` | **Error-only (no success toast):** Status toggle mutations in `routes/tasks.tsx`, `KanbanBoard.tsx`, `ProjectDetail.tsx`, `ChatEntityBlock.tsx` — visual feedback (badge/card move) IS the confirmation. ### Projects | File | Mutation | Toast | Key | |------|----------|-------|-----| | `components/projects/ProjectSidebar.tsx` | `projects.create` | success | `toast.project.created` | | `components/projects/ProjectSidebar.tsx` | `projects.update` | success | `toast.project.updated` | | `components/projects/ProjectSidebar.tsx` | `projects.delete` | warning | `toast.project.deleted` | | `components/projects/ProjectSidebar.tsx` | `projects.archiveByClient` | warning | `toast.project.archivedAll` / `unarchivedAll` | ### Clients | File | Mutation | Toast | Key | |------|----------|-------|-----| | `components/projects/ProjectSidebar.tsx` | `clients.create` | success | `toast.client.created` | | `components/projects/ProjectSidebar.tsx` | `clients.update` | success | `toast.client.updated` | | `components/projects/ProjectSidebar.tsx` | `clients.deleteWithCascade` | warning | `toast.client.deleted` | | `components/tasks/NewTaskDialog.tsx` | `clients.create` (inline) | success | `toast.client.created` | ### Notes | File | Mutation | Toast | Key | |------|----------|-------|-----| | `components/projects/ProjectDetail.tsx` | `notes.create` | success | `toast.note.created` | | `routes/notes.$noteId.tsx` | `notes.delete` | warning | `toast.note.deleted` | ### Timeline Events | File | Mutation | Toast | Key | |------|----------|-------|-----| | `components/timeline/AddEventDialog.tsx` | `timelineEvents.create` | success | `toast.timeline.created` | | `components/timeline/EditEventDialog.tsx` | `timelineEvents.update` | success | `toast.timeline.updated` | | `routes/timeline.tsx` | `timelineEvents.delete` | warning | `toast.timeline.deleted` | | `routes/timeline.tsx` | `timelineEvents.update` | success | `toast.timeline.updated` | ### Agents | File | Mutation | Toast | Key | |------|----------|-------|-----| | `components/settings/AgentsSection.tsx` | `agent.*.delete` | warning | `toast.agent.deleted` | | `components/settings/AgentsSection.tsx` | `agent.runNow` | promise | `toast.agent.runStarted` / `runError` | | `components/settings/InlineAgentCreationStepper.tsx` | `agent.*.create` | success | `toast.agent.created` | --- ## Phase 4: Auth + Onboarding | File | Mutation | Toast | Key | |------|----------|-------|-----| | `components/auth/LoginForm.tsx` | `auth.login` error | error | `toast.auth.loginError` | | `components/auth/LoginForm.tsx` | `auth.register` error | error | `toast.auth.registerError` | | `components/auth/LoginForm.tsx` | `auth.loginWithOAuth` error | error | `toast.auth.oauthError` | | `components/layout/AppShell.tsx` | `auth.logout` success | info | `toast.auth.loggedOut` | | `components/onboarding/OnboardingFlow.tsx` | final save success | success | `toast.onboarding.completed` | | `components/onboarding/OnboardingFlow.tsx` | normalize call | promise | `toast.onboarding.normalizing` / `normalized` | **Auth note:** Keep inline form errors in LoginForm alongside toast — form-level positional context is valuable. --- ## Explicitly SILENT Mutations (no toast) | Mutation | Reason | |----------|--------| | `notes.update` (auto-save debounced 2s) | Would spam a toast every 2s while typing | | `tasks.update` status toggle (kanban drag, checkbox, status cycle) | Card movement / badge change IS the feedback | | `settings.setSidebarCollapsed` | Sidebar animation IS the feedback | | `ai.chat` / `ai.dailyBrief` | Has own streaming UI | | `agent.journey.*` | Has own conversational UI | For these: add `onError` callback only (no success toast). --- ## All Files Modified (Summary) **New files (2):** - `src/renderer/components/ui/sonner.tsx` — generated by CLI + theme fix - `src/renderer/hooks/useNotify.ts` — custom hook **Modified files (~25):** - `package.json` — `sonner` dependency (automatic via CLI) - `src/renderer/index.tsx` — add `` - 5x `locales/{en,it,es,fr,de}/translation.json` — add `toast` keys - 5x Settings components — replace saved state with toast - ~14x CRUD/auth/onboarding components — add toast calls --- ## Verification 1. **All app states:** Trigger toasts during login (error), onboarding (completion), and normal usage (CRUD) 2. **Theme:** Switch dark/light/system — toast backgrounds should follow semantic tokens 3. **i18n:** Switch to Italian → trigger save → toast should read "Profilo aggiornato" 4. **Error persistence:** Error toasts stay until dismissed (test with invalid backend URL) 5. **Silent mutations:** Drag task on kanban, type in notes, toggle sidebar — NO toasts 6. **Removed patterns:** Settings Save buttons stay as "Save" (no "Saved" flash) + toast appears 7. **Position:** Bottom-right, not overlapping sidebar 8. **Lint:** `npm run lint` passes 9. **Build:** `npm run package` succeeds