update skill config
This commit is contained in:
484
docs/plan-sonner-notifications.md
Normal file
484
docs/plan-sonner-notifications.md
Normal file
@@ -0,0 +1,484 @@
|
||||
# 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<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>`:
|
||||
|
||||
```tsx
|
||||
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`
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```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 `<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 `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 `<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
|
||||
Reference in New Issue
Block a user