Files
workspace/docs/plan-sonner-notifications.md
2026-04-15 11:26:46 +02:00

28 KiB

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

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:

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:

import { useTheme } from "@/components/theme-provider"
import { Toaster as Sonner } from "sonner"

type ToasterProps = React.ComponentProps<typeof Sonner>

const Toaster = ({ ...props }: ToasterProps) => {
  const { theme = "system" } = useTheme()
  return (
    <Sonner
      theme={theme as ToasterProps["theme"]}
      position="bottom-right"
      richColors
      className="toaster group"
      toastOptions={{
        classNames: {
          toast:
            "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
          description: "group-[.toast]:text-muted-foreground",
          actionButton:
            "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
          cancelButton:
            "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
        },
      }}
      {...props}
    />
  )
}

export { Toaster }

1.3 Place <Toaster /> in src/renderer/index.tsx

Add <Toaster /> as a sibling of <RouterProvider /> inside <ThemeProvider>:

import { Toaster } from '@/components/ui/sonner';
// ...
<ThemeProvider defaultTheme="system" storageKey="adiuvai-theme">
  <trpc.Provider client={trpcClient} queryClient={queryClient}>
    <QueryClientProvider client={queryClient}>
      <LanguageSync />
      <RouterProvider router={router} />
      <Toaster />
    </QueryClientProvider>
  </trpc.Provider>
</ThemeProvider>

Why here? The <Toaster /> 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

import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';

type ToastVariant = 'success' | 'error' | 'info' | 'warning';

interface NotifyOptions {
  descriptionKey?: string;
  values?: Record<string, string | number>;
  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<T>(promise: Promise<T>, 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:

"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:

"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:

"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:

"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:

"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 <p> 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 <p> 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 setTimeouts
  2. Profile save onSuccessnotify('success', 'toast.settings.memorySaved')
  3. Display save onSuccessnotify('success', 'toast.settings.formatPrefsSaved')
  4. Reset onboarding onSuccessnotify('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 onSuccessnotify('success', 'toast.settings.backendUrlSaved')
  3. Add onErrornotifyError('toast.settings.backendUrlError', err)
  4. Logout onSuccessnotify('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 onSuccessnotify('success', 'toast.agent.updated')
  3. Add onErrornotifyError('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.jsonsonner dependency (automatic via CLI)
  • src/renderer/index.tsx — add <Toaster />
  • 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