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 foronErrorcallbacks — title from i18n, description from raw errornotifyPromise: wrapstoast.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:
- Add
const { notify, notifyError } = useNotify(); - Remove
const [saved, setSaved] = useState(false); - Remove
const [error, setError] = useState(''); - In
handleSaveonSuccess: removesetSaved(true); setTimeout(...)→ addnotify('success', 'toast.profile.updated'); - In
handleSaveonError: replacesetError(err.message)→notifyError('toast.profile.updateError', err); - Remove inline error
<p>tag (line 93) - Remove
setSaved(false)fromonChangehandlers (lines 83, 89) - Button text:
{saved ? t('settings.saved') : t('common.save')}→{t('common.save')} - In
handleLanguageChange: addnotify('info', 'toast.settings.languageChanged');
2.2 src/renderer/components/settings/ProfileSection.tsx
Current: profileSaved/displaySaved states, both with setTimeout.
Changes:
- Add
useNotify(), remove bothsavedstates andsetTimeouts - Profile save
onSuccess→notify('success', 'toast.settings.memorySaved') - Display save
onSuccess→notify('success', 'toast.settings.formatPrefsSaved') - Reset onboarding
onSuccess→notify('info', 'toast.onboarding.reset') - Both save buttons: replace ternary with
{t('common.save')}
2.3 src/renderer/components/settings/AccountSection.tsx
Current: urlSaved/setUrlSaved state, setTimeout.
Changes:
- Add
useNotify(), removeurlSavedstate andsetTimeout - Backend URL save
onSuccess→notify('success', 'toast.settings.backendUrlSaved') - Add
onError→notifyError('toast.settings.backendUrlError', err) - Logout
onSuccess→notify('info', 'toast.auth.loggedOut') - Button text: replace ternary with
{t('common.save')}
2.4 src/renderer/components/settings/LocalAgentConfigPanel.tsx
Current: saved/setSaved state, setTimeout.
Changes:
- Add
useNotify(), removesavedstate andsetTimeout - Save
onSuccess→notify('success', 'toast.agent.updated') - Add
onError→notifyError('toast.agent.updateError', err) - 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 fixsrc/renderer/hooks/useNotify.ts— custom hook
Modified files (~25):
package.json—sonnerdependency (automatic via CLI)src/renderer/index.tsx— add<Toaster />- 5x
locales/{en,it,es,fr,de}/translation.json— addtoastkeys - 5x Settings components — replace saved state with toast
- ~14x CRUD/auth/onboarding components — add toast calls
Verification
- All app states: Trigger toasts during login (error), onboarding (completion), and normal usage (CRUD)
- Theme: Switch dark/light/system — toast backgrounds should follow semantic tokens
- i18n: Switch to Italian → trigger save → toast should read "Profilo aggiornato"
- Error persistence: Error toasts stay until dismissed (test with invalid backend URL)
- Silent mutations: Drag task on kanban, type in notes, toggle sidebar — NO toasts
- Removed patterns: Settings Save buttons stay as "Save" (no "Saved" flash) + toast appears
- Position: Bottom-right, not overlapping sidebar
- Lint:
npm run lintpasses - Build:
npm run packagesucceeds