From f048db7b52b9986af3f292c7de1a4b3a629fd8a8 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:33:45 +0200 Subject: [PATCH] fix: count each plugin hook failure once and keep a plugin that kept failing turned off --- components/plugins/plugin-dialog-host.tsx | 13 +++++++++++ components/settings/plugins-settings.tsx | 2 +- lib/__tests__/plugin-sandbox-loader.test.ts | 24 +++++++++++++++++++++ lib/plugin-loader.ts | 5 ++++- lib/plugin-sandbox/loader.ts | 24 ++++++++++++--------- locales/ar/common.json | 6 ++++-- locales/ca/common.json | 6 ++++-- locales/cs/common.json | 6 ++++-- locales/da/common.json | 6 ++++-- locales/de/common.json | 6 ++++-- locales/en/common.json | 6 ++++-- locales/es/common.json | 6 ++++-- locales/fa/common.json | 6 ++++-- locales/fr/common.json | 6 ++++-- locales/he/common.json | 6 ++++-- locales/hu/common.json | 6 ++++-- locales/it/common.json | 6 ++++-- locales/ja/common.json | 6 ++++-- locales/ko/common.json | 6 ++++-- locales/lv/common.json | 6 ++++-- locales/mn/common.json | 6 ++++-- locales/nb/common.json | 6 ++++-- locales/nl/common.json | 6 ++++-- locales/pl/common.json | 6 ++++-- locales/pt/common.json | 6 ++++-- locales/ro/common.json | 6 ++++-- locales/ru/common.json | 6 ++++-- locales/sk/common.json | 6 ++++-- locales/tr/common.json | 6 ++++-- locales/uk/common.json | 6 ++++-- locales/zh-TW/common.json | 6 ++++-- locales/zh/common.json | 6 ++++-- stores/plugin-store.ts | 23 ++++++++++++++++---- 33 files changed, 183 insertions(+), 70 deletions(-) diff --git a/components/plugins/plugin-dialog-host.tsx b/components/plugins/plugin-dialog-host.tsx index fcdee26d2..856a74f04 100644 --- a/components/plugins/plugin-dialog-host.tsx +++ b/components/plugins/plugin-dialog-host.tsx @@ -10,6 +10,7 @@ import React, { useEffect, useId, useMemo, useState, useSyncExternalStore } from import { useTranslations } from 'next-intl'; import { cancelForPlugin, head, resolveDialog, resolveHead, subscribe } from '@/lib/plugin-sandbox/host-dialog'; import { usePluginStore } from '@/stores/plugin-store'; +import { toast } from '@/stores/toast-store'; import { Button } from '@/components/ui/button'; import { Puzzle, X } from '@/components/icons'; import { PluginIframeSlot } from './plugin-iframe-slot'; @@ -35,6 +36,18 @@ export function PluginDialogHost(): React.JSX.Element | null { const pluginName = usePluginStore((s) => s.plugins.find((p) => p.id === current?.pluginId)?.name); const disablePlugin = usePluginStore((s) => s.disablePlugin); + // Say so when a plugin was turned off for failing; otherwise it just + // stops working with no hint why. + useEffect(() => { + const onDisabled = (e: Event) => { + const id = (e as CustomEvent<{ pluginId: string }>).detail?.pluginId; + const name = usePluginStore.getState().plugins.find((p) => p.id === id)?.name ?? id; + if (id) toast.warning(t('toast_auto_disabled', { name })); + }; + window.addEventListener('plugin:auto-disabled', onDisabled); + return () => window.removeEventListener('plugin:auto-disabled', onDisabled); + }, [t]); + const isPrompt = current?.kind === 'prompt'; const fields = useMemo(() => (isPrompt ? current?.fields ?? [] : []), [isPrompt, current]); const hasPassword = fields.some((f) => f.type === 'password'); diff --git a/components/settings/plugins-settings.tsx b/components/settings/plugins-settings.tsx index 07bc35aa5..f58bb1b44 100644 --- a/components/settings/plugins-settings.tsx +++ b/components/settings/plugins-settings.tsx @@ -24,7 +24,7 @@ const STATUS_COLORS: Record = { }; /** Errors the store records as codes, so they can be shown translated. */ -const ERROR_CODES = new Set(['user-plugins-off', 'approval-unavailable', 'approval-denied', 'approval-pending']); +const ERROR_CODES = new Set(['user-plugins-off', 'approval-unavailable', 'approval-denied', 'approval-pending', 'auto-disabled']); export function PluginsSettings() { const t = useTranslations('plugins.settings'); diff --git a/lib/__tests__/plugin-sandbox-loader.test.ts b/lib/__tests__/plugin-sandbox-loader.test.ts index 70e0b3b03..d660a6776 100644 --- a/lib/__tests__/plugin-sandbox-loader.test.ts +++ b/lib/__tests__/plugin-sandbox-loader.test.ts @@ -219,3 +219,27 @@ describe('setSandboxLocale', () => { expect(mocks.setLocale).toHaveBeenCalledWith('de'); }); }); + +describe('failing hooks', () => { + it('turns a plugin off on its third failure, not its second', async () => { + const { emailHooks, pluginErrorTracker } = await import('../plugin-hooks'); + const { setupSandboxAutoDisable } = await import('../plugin-sandbox/loader'); + const markAutoDisabled = vi.fn(); + setSandboxStoreAccessor({ setPluginStatus: () => {}, markAutoDisabled }); + setupSandboxAutoDisable(); + pluginErrorTracker.reset('managed-plugin'); + mocks.getCode.mockResolvedValue(CODE); + mocks.createBackgroundInstance.mockImplementation(() => ({ + ...fakeBackground(), + initPromise: Promise.resolve({ hooks: ['onEmailOpen'], slots: [], shortcuts: [] }), + invokeHook: vi.fn(() => Promise.reject(new Error('boom'))), + })); + await loadSandboxedPlugin(plugin({ permissions: ['email:read'] })); + + await emailHooks.onEmailOpen.emit({}); + await emailHooks.onEmailOpen.emit({}); + expect(markAutoDisabled).not.toHaveBeenCalled(); + await emailHooks.onEmailOpen.emit({}); + expect(markAutoDisabled).toHaveBeenCalledWith('managed-plugin'); + }); +}); diff --git a/lib/plugin-loader.ts b/lib/plugin-loader.ts index a4b1aa091..747f5f7f3 100644 --- a/lib/plugin-loader.ts +++ b/lib/plugin-loader.ts @@ -33,7 +33,10 @@ export function exposePluginExternals(): void { // ─── Store accessor (status updates) ────────────────────────── -type StoreAccessor = { setPluginStatus: (id: string, status: InstalledPlugin['status'], error?: string) => void }; +type StoreAccessor = { + setPluginStatus: (id: string, status: InstalledPlugin['status'], error?: string) => void; + markAutoDisabled?: (id: string) => void; +}; export function setPluginStoreAccessor(accessor: StoreAccessor): void { setSandboxStoreAccessor(accessor); diff --git a/lib/plugin-sandbox/loader.ts b/lib/plugin-sandbox/loader.ts index d7ecf6fe0..b06bfde91 100644 --- a/lib/plugin-sandbox/loader.ts +++ b/lib/plugin-sandbox/loader.ts @@ -39,7 +39,11 @@ const HOOK_BUSES: Record = Object.assign({}, // ─── Store accessor (status updates flow through the existing store) ── -type StoreAccessor = { setPluginStatus: (id: string, status: InstalledPlugin['status'], error?: string) => void }; +type StoreAccessor = { + setPluginStatus: (id: string, status: InstalledPlugin['status'], error?: string) => void; + /** Turn a failing plugin off for good (until the user turns it on again). */ + markAutoDisabled?: (id: string) => void; +}; let storeAccessor: StoreAccessor | null = null; export function setSandboxStoreAccessor(a: StoreAccessor): void { storeAccessor = a; } @@ -187,14 +191,9 @@ export async function loadSandboxedPlugin(plugin: InstalledPlugin): Promise { - try { - return await bg.invokeHook(hookName, args); - } catch (err) { - pluginErrorTracker.record(plugin.id, err); - throw err; - } - }; + // The bus records a failure once; recording it here too made the + // three-strikes breaker trip on the second error. + const proxy = (...args: unknown[]) => bg.invokeHook(hookName, args); hookDisposables.push(bus.register(plugin.id, proxy as (...a: unknown[]) => unknown)); } @@ -277,7 +276,12 @@ export function setupSandboxAutoDisable(): void { setHookTimeoutListener((pluginId) => cancelPluginDialogs(pluginId)); pluginErrorTracker.setAutoDisableCallback((pluginId) => { unloadSandboxedPlugin(pluginId); - storeAccessor?.setPluginStatus(pluginId, 'error', 'Auto-disabled due to repeated errors'); + if (storeAccessor?.markAutoDisabled) storeAccessor.markAutoDisabled(pluginId); + else storeAccessor?.setPluginStatus(pluginId, 'error', 'auto-disabled'); + // PluginDialogHost turns this into a toast in the user's language. + if (typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent('plugin:auto-disabled', { detail: { pluginId } })); + } }); } diff --git a/locales/ar/common.json b/locales/ar/common.json index d63070241..8ffea83c2 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "أوقف المسؤول الإضافات التي يثبّتها المستخدمون، لذا لا تعمل هذه الإضافة.", "error_approval_unavailable": "ثُبّتت هذه الإضافة قبل أن تصبح الموافقة مطلوبة. أزلها وثبّتها مرة أخرى لإرسالها للموافقة.", "error_approval_denied": "لم يوافق المسؤول على هذه الإضافة.", - "error_approval_pending": "بانتظار موافقة المسؤول على هذه الإضافة." + "error_approval_pending": "بانتظار موافقة المسؤول على هذه الإضافة.", + "error_auto_disabled": "تم إيقافها لأنها فشلت مرارًا. شغّلها مرة أخرى للمحاولة من جديد." }, "consent": { "title": "هل تسمح لـ {name} بذلك؟", @@ -4250,7 +4251,8 @@ "cancel": "إلغاء", "close": "إغلاق", "turn_off": "إيقاف {name}", - "password_note": "هذا النموذج من الإضافة {name} وليس من بريد الويب. لا تدخل كلمة مرور إلا إذا كنت تثق بهذه الإضافة." + "password_note": "هذا النموذج من الإضافة {name} وليس من بريد الويب. لا تدخل كلمة مرور إلا إذا كنت تثق بهذه الإضافة.", + "toast_auto_disabled": "تم إيقاف {name} لأنها فشلت مرارًا." } }, "global_search": { diff --git a/locales/ca/common.json b/locales/ca/common.json index 9b8c72810..5531a18fe 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "L'administrador ha desactivat els connectors instal·lats pels usuaris, així que aquest connector no s'executa.", "error_approval_unavailable": "Aquest connector es va instal·lar abans que calgués l'aprovació. Elimineu-lo i torneu-lo a instal·lar per enviar-lo a aprovar.", "error_approval_denied": "L'administrador no ha aprovat aquest connector.", - "error_approval_pending": "S'espera que l'administrador aprovi aquest connector." + "error_approval_pending": "S'espera que l'administrador aprovi aquest connector.", + "error_auto_disabled": "S'ha desactivat perquè fallava repetidament. Torneu-lo a activar per tornar-ho a provar." }, "consent": { "title": "Voleu permetre que {name} faci això?", @@ -4250,7 +4251,8 @@ "cancel": "Cancel·la", "close": "Tanca", "turn_off": "Desactiva {name}", - "password_note": "Aquest formulari ve del connector {name}, no del correu web. Introduïu una contrasenya només si confieu en aquest connector." + "password_note": "Aquest formulari ve del connector {name}, no del correu web. Introduïu una contrasenya només si confieu en aquest connector.", + "toast_auto_disabled": "{name} s'ha desactivat perquè fallava repetidament." } }, "global_search": { diff --git a/locales/cs/common.json b/locales/cs/common.json index 925b79239..c618351a8 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Váš správce vypnul pluginy instalované uživateli, proto tento plugin neběží.", "error_approval_unavailable": "Tento plugin byl nainstalován dříve, než bylo vyžadováno schválení. Odstraňte ho a nainstalujte znovu, aby se odeslal ke schválení.", "error_approval_denied": "Váš správce tento plugin neschválil.", - "error_approval_pending": "Čeká se, až správce tento plugin schválí." + "error_approval_pending": "Čeká se, až správce tento plugin schválí.", + "error_auto_disabled": "Vypnuto, protože opakovaně selhávalo. Zapněte ho znovu a zkuste to znovu." }, "consent": { "title": "Povolit pluginu {name} tyto akce?", @@ -4250,7 +4251,8 @@ "cancel": "Zrušit", "close": "Zavřít", "turn_off": "Vypnout {name}", - "password_note": "Tento formulář pochází z pluginu {name}, ne z webmailu. Heslo zadejte jen tehdy, když tomuto pluginu důvěřujete." + "password_note": "Tento formulář pochází z pluginu {name}, ne z webmailu. Heslo zadejte jen tehdy, když tomuto pluginu důvěřujete.", + "toast_auto_disabled": "{name} byl vypnut, protože opakovaně selhával." } }, "global_search": { diff --git a/locales/da/common.json b/locales/da/common.json index 77310b3b1..d263b1602 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Din administrator har slået plugins installeret af brugere fra, så dette plugin kører ikke.", "error_approval_unavailable": "Dette plugin blev installeret, før godkendelse var påkrævet. Fjern det, og installer det igen for at sende det til godkendelse.", "error_approval_denied": "Din administrator har ikke godkendt dette plugin.", - "error_approval_pending": "Venter på, at din administrator godkender dette plugin." + "error_approval_pending": "Venter på, at din administrator godkender dette plugin.", + "error_auto_disabled": "Slået fra, fordi det blev ved med at fejle. Slå det til igen for at prøve igen." }, "consent": { "title": "Vil du tillade {name} at gøre dette?", @@ -4250,7 +4251,8 @@ "cancel": "Annuller", "close": "Luk", "turn_off": "Slå {name} fra", - "password_note": "Denne formular kommer fra pluginet {name}, ikke fra webmailen. Indtast kun en adgangskode, hvis du stoler på dette plugin." + "password_note": "Denne formular kommer fra pluginet {name}, ikke fra webmailen. Indtast kun en adgangskode, hvis du stoler på dette plugin.", + "toast_auto_disabled": "{name} blev slået fra, fordi det blev ved med at fejle." } }, "global_search": { diff --git a/locales/de/common.json b/locales/de/common.json index 7f09db870..12eb3a71a 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Ihr Administrator hat von Benutzern installierte Plugins ausgeschaltet. Dieses Plugin läuft daher nicht.", "error_approval_unavailable": "Dieses Plugin wurde installiert, bevor eine Freigabe nötig war. Entfernen Sie es und installieren Sie es erneut, um es zur Freigabe vorzulegen.", "error_approval_denied": "Ihr Administrator hat dieses Plugin nicht freigegeben.", - "error_approval_pending": "Wartet darauf, dass Ihr Administrator dieses Plugin freigibt." + "error_approval_pending": "Wartet darauf, dass Ihr Administrator dieses Plugin freigibt.", + "error_auto_disabled": "Ausgeschaltet, weil es wiederholt fehlgeschlagen ist. Schalten Sie es wieder ein, um es erneut zu versuchen." }, "consent": { "title": "{name} Folgendes erlauben?", @@ -4250,7 +4251,8 @@ "cancel": "Abbrechen", "close": "Schließen", "turn_off": "{name} ausschalten", - "password_note": "Dieses Formular stammt vom Plugin {name}, nicht vom Webmail. Geben Sie ein Passwort nur ein, wenn Sie diesem Plugin vertrauen." + "password_note": "Dieses Formular stammt vom Plugin {name}, nicht vom Webmail. Geben Sie ein Passwort nur ein, wenn Sie diesem Plugin vertrauen.", + "toast_auto_disabled": "{name} wurde ausgeschaltet, weil es wiederholt fehlgeschlagen ist." } }, "global_search": { diff --git a/locales/en/common.json b/locales/en/common.json index 5ba9ae751..685016f2d 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Your administrator turned off plugins installed by users, so this plugin does not run.", "error_approval_unavailable": "This plugin was installed before approvals were required. Remove it and install it again to send it for approval.", "error_approval_denied": "Your administrator did not approve this plugin.", - "error_approval_pending": "Waiting for your administrator to approve this plugin." + "error_approval_pending": "Waiting for your administrator to approve this plugin.", + "error_auto_disabled": "Turned off because it kept failing. Turn it on again to retry." }, "consent": { "title": "Allow {name} to do this?", @@ -4250,7 +4251,8 @@ "cancel": "Cancel", "close": "Close", "turn_off": "Turn off {name}", - "password_note": "This form comes from the plugin {name}, not from the webmail. Only enter a password if you trust this plugin." + "password_note": "This form comes from the plugin {name}, not from the webmail. Only enter a password if you trust this plugin.", + "toast_auto_disabled": "{name} was turned off because it kept failing." } }, "global_search": { diff --git a/locales/es/common.json b/locales/es/common.json index 3d60e509d..603052683 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Tu administrador desactivó los complementos instalados por usuarios, así que este complemento no se ejecuta.", "error_approval_unavailable": "Este complemento se instaló antes de que se exigiera aprobación. Elimínalo e instálalo de nuevo para enviarlo a aprobación.", "error_approval_denied": "Tu administrador no aprobó este complemento.", - "error_approval_pending": "Esperando a que tu administrador apruebe este complemento." + "error_approval_pending": "Esperando a que tu administrador apruebe este complemento.", + "error_auto_disabled": "Desactivado porque fallaba una y otra vez. Actívalo de nuevo para volver a intentarlo." }, "consent": { "title": "¿Permitir que {name} haga esto?", @@ -4250,7 +4251,8 @@ "cancel": "Cancelar", "close": "Cerrar", "turn_off": "Desactivar {name}", - "password_note": "Este formulario viene del complemento {name}, no del correo web. Introduce una contraseña solo si confías en este complemento." + "password_note": "Este formulario viene del complemento {name}, no del correo web. Introduce una contraseña solo si confías en este complemento.", + "toast_auto_disabled": "{name} se desactivó porque fallaba una y otra vez." } }, "global_search": { diff --git a/locales/fa/common.json b/locales/fa/common.json index 69d5e7445..6726945b8 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "مدیر سیستم افزونه‌هایی را که کاربران نصب می‌کنند خاموش کرده است، بنابراین این افزونه اجرا نمی‌شود.", "error_approval_unavailable": "این افزونه پیش از الزامی شدن تأیید نصب شده است. آن را حذف و دوباره نصب کنید تا برای تأیید ارسال شود.", "error_approval_denied": "مدیر سیستم این افزونه را تأیید نکرد.", - "error_approval_pending": "در انتظار تأیید این افزونه توسط مدیر سیستم." + "error_approval_pending": "در انتظار تأیید این افزونه توسط مدیر سیستم.", + "error_auto_disabled": "به‌دلیل خطاهای پی‌درپی خاموش شد. برای تلاش دوباره آن را روشن کنید." }, "consent": { "title": "به {name} اجازه این کار داده شود؟", @@ -4250,7 +4251,8 @@ "cancel": "لغو", "close": "بستن", "turn_off": "خاموش کردن {name}", - "password_note": "این فرم از افزونه {name} است، نه از وب‌میل. فقط اگر به این افزونه اعتماد دارید گذرواژه وارد کنید." + "password_note": "این فرم از افزونه {name} است، نه از وب‌میل. فقط اگر به این افزونه اعتماد دارید گذرواژه وارد کنید.", + "toast_auto_disabled": "{name} به‌دلیل خطاهای پی‌درپی خاموش شد." } }, "global_search": { diff --git a/locales/fr/common.json b/locales/fr/common.json index 8bfe55d18..27ed6886d 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Votre administrateur a désactivé les plugins installés par les utilisateurs, ce plugin ne s'exécute donc pas.", "error_approval_unavailable": "Ce plugin a été installé avant que l'approbation soit requise. Supprimez-le et réinstallez-le pour l'envoyer en approbation.", "error_approval_denied": "Votre administrateur n'a pas approuvé ce plugin.", - "error_approval_pending": "En attente de l'approbation de ce plugin par votre administrateur." + "error_approval_pending": "En attente de l'approbation de ce plugin par votre administrateur.", + "error_auto_disabled": "Désactivé parce qu'il échouait sans cesse. Réactivez-le pour réessayer." }, "consent": { "title": "Autoriser {name} à faire ceci ?", @@ -4250,7 +4251,8 @@ "cancel": "Annuler", "close": "Fermer", "turn_off": "Désactiver {name}", - "password_note": "Ce formulaire provient du plugin {name}, pas du webmail. Ne saisissez un mot de passe que si vous faites confiance à ce plugin." + "password_note": "Ce formulaire provient du plugin {name}, pas du webmail. Ne saisissez un mot de passe que si vous faites confiance à ce plugin.", + "toast_auto_disabled": "{name} a été désactivé parce qu'il échouait sans cesse." } }, "global_search": { diff --git a/locales/he/common.json b/locales/he/common.json index 7682a11c1..f1f7fdfcb 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -4017,7 +4017,8 @@ "error_user_plugins_off": "מנהל המערכת כיבה תוספים שמשתמשים התקינו, ולכן התוסף הזה לא פועל.", "error_approval_unavailable": "התוסף הזה הותקן לפני שנדרש אישור. הסירו אותו והתקינו אותו מחדש כדי לשלוח אותו לאישור.", "error_approval_denied": "מנהל המערכת לא אישר את התוסף הזה.", - "error_approval_pending": "ממתין לאישור התוסף הזה על ידי מנהל המערכת." + "error_approval_pending": "ממתין לאישור התוסף הזה על ידי מנהל המערכת.", + "error_auto_disabled": "כובה כי נכשל שוב ושוב. הפעילו אותו מחדש כדי לנסות שוב." }, "consent": { "title": "לאפשר ל-{name} לעשות זאת?", @@ -4251,7 +4252,8 @@ "cancel": "ביטול", "close": "סגירה", "turn_off": "כיבוי {name}", - "password_note": "הטופס הזה מגיע מהתוסף {name}, לא מהדואר האינטרנטי. הזינו סיסמה רק אם אתם סומכים על התוסף הזה." + "password_note": "הטופס הזה מגיע מהתוסף {name}, לא מהדואר האינטרנטי. הזינו סיסמה רק אם אתם סומכים על התוסף הזה.", + "toast_auto_disabled": "{name} כובה כי נכשל שוב ושוב." } }, "global_search": { diff --git a/locales/hu/common.json b/locales/hu/common.json index 285b6f2b9..b2b779e6f 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "A rendszergazda kikapcsolta a felhasználók által telepített bővítményeket, ezért ez a bővítmény nem fut.", "error_approval_unavailable": "Ezt a bővítményt még azelőtt telepítetted, hogy jóváhagyás kellett volna. Távolítsd el, majd telepítsd újra, hogy jóváhagyásra kerüljön.", "error_approval_denied": "A rendszergazda nem hagyta jóvá ezt a bővítményt.", - "error_approval_pending": "Várakozás arra, hogy a rendszergazda jóváhagyja ezt a bővítményt." + "error_approval_pending": "Várakozás arra, hogy a rendszergazda jóváhagyja ezt a bővítményt.", + "error_auto_disabled": "Kikapcsolva, mert többször is hibát okozott. Kapcsolja be újra, ha ismét meg szeretné próbálni." }, "consent": { "title": "{name}: engedélyezed ezeket a műveleteket?", @@ -4250,7 +4251,8 @@ "cancel": "Mégse", "close": "Bezárás", "turn_off": "{name} kikapcsolása", - "password_note": "Ez az űrlap a(z) {name} bővítménytől származik, nem a webmailtől. Csak akkor adjon meg jelszót, ha megbízik ebben a bővítményben." + "password_note": "Ez az űrlap a(z) {name} bővítménytől származik, nem a webmailtől. Csak akkor adjon meg jelszót, ha megbízik ebben a bővítményben.", + "toast_auto_disabled": "{name} ki lett kapcsolva, mert többször is hibát okozott." } }, "global_search": { diff --git a/locales/it/common.json b/locales/it/common.json index 5e6c81183..beb5e13a9 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Il tuo amministratore ha disattivato i plugin installati dagli utenti, quindi questo plugin non viene eseguito.", "error_approval_unavailable": "Questo plugin è stato installato prima che fosse richiesta l'approvazione. Rimuovilo e installalo di nuovo per inviarlo in approvazione.", "error_approval_denied": "Il tuo amministratore non ha approvato questo plugin.", - "error_approval_pending": "In attesa che il tuo amministratore approvi questo plugin." + "error_approval_pending": "In attesa che il tuo amministratore approvi questo plugin.", + "error_auto_disabled": "Disattivato perché continuava a non funzionare. Riattivalo per riprovare." }, "consent": { "title": "Consentire a {name} di fare questo?", @@ -4250,7 +4251,8 @@ "cancel": "Annulla", "close": "Chiudi", "turn_off": "Disattiva {name}", - "password_note": "Questo modulo proviene dal plugin {name}, non dalla webmail. Inserisci una password solo se ti fidi di questo plugin." + "password_note": "Questo modulo proviene dal plugin {name}, non dalla webmail. Inserisci una password solo se ti fidi di questo plugin.", + "toast_auto_disabled": "{name} è stato disattivato perché continuava a non funzionare." } }, "global_search": { diff --git a/locales/ja/common.json b/locales/ja/common.json index c325571e3..5288d58b1 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "管理者がユーザーによるプラグインを無効にしたため、このプラグインは動作しません。", "error_approval_unavailable": "このプラグインは承認が必要になる前にインストールされました。承認を依頼するには、削除してもう一度インストールしてください。", "error_approval_denied": "管理者はこのプラグインを承認しませんでした。", - "error_approval_pending": "管理者がこのプラグインを承認するのを待っています。" + "error_approval_pending": "管理者がこのプラグインを承認するのを待っています。", + "error_auto_disabled": "繰り返し失敗したためオフになりました。再試行するにはもう一度オンにしてください。" }, "consent": { "title": "{name} にこの操作を許可しますか?", @@ -4250,7 +4251,8 @@ "cancel": "キャンセル", "close": "閉じる", "turn_off": "{name} をオフにする", - "password_note": "このフォームはウェブメールではなく、プラグイン {name} が表示しています。このプラグインを信頼できる場合にのみパスワードを入力してください。" + "password_note": "このフォームはウェブメールではなく、プラグイン {name} が表示しています。このプラグインを信頼できる場合にのみパスワードを入力してください。", + "toast_auto_disabled": "{name} は繰り返し失敗したためオフになりました。" } }, "global_search": { diff --git a/locales/ko/common.json b/locales/ko/common.json index 69c9ef73b..0ce740141 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "관리자가 사용자가 설치한 플러그인을 꺼 두어서 이 플러그인은 실행되지 않아요.", "error_approval_unavailable": "이 플러그인은 승인이 필요해지기 전에 설치되었어요. 승인을 요청하려면 삭제한 뒤 다시 설치하세요.", "error_approval_denied": "관리자가 이 플러그인을 승인하지 않았어요.", - "error_approval_pending": "관리자가 이 플러그인을 승인하기를 기다리고 있어요." + "error_approval_pending": "관리자가 이 플러그인을 승인하기를 기다리고 있어요.", + "error_auto_disabled": "계속 실패해서 꺼졌습니다. 다시 시도하려면 다시 켜세요." }, "consent": { "title": "{name}에 이 권한을 허용할까요?", @@ -4250,7 +4251,8 @@ "cancel": "취소", "close": "닫기", "turn_off": "{name} 끄기", - "password_note": "이 양식은 웹메일이 아니라 플러그인 {name}에서 표시한 것입니다. 이 플러그인을 신뢰할 때만 비밀번호를 입력하세요." + "password_note": "이 양식은 웹메일이 아니라 플러그인 {name}에서 표시한 것입니다. 이 플러그인을 신뢰할 때만 비밀번호를 입력하세요.", + "toast_auto_disabled": "{name}이(가) 계속 실패해서 꺼졌습니다." } }, "global_search": { diff --git a/locales/lv/common.json b/locales/lv/common.json index ed70a2ad2..bc8638beb 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Jūsu administrators ir izslēdzis lietotāju instalētos spraudņus, tāpēc šis spraudnis nedarbojas.", "error_approval_unavailable": "Šis spraudnis tika instalēts, pirms bija vajadzīgs apstiprinājums. Noņemiet to un instalējiet vēlreiz, lai nosūtītu apstiprināšanai.", "error_approval_denied": "Jūsu administrators neapstiprināja šo spraudni.", - "error_approval_pending": "Gaida, kad jūsu administrators apstiprinās šo spraudni." + "error_approval_pending": "Gaida, kad jūsu administrators apstiprinās šo spraudni.", + "error_auto_disabled": "Izslēgts, jo atkārtoti neizdevās. Ieslēdziet to vēlreiz, lai mēģinātu vēlreiz." }, "consent": { "title": "Atļaut spraudnim {name} to darīt?", @@ -4250,7 +4251,8 @@ "cancel": "Atcelt", "close": "Aizvērt", "turn_off": "Izslēgt {name}", - "password_note": "Šī veidlapa nāk no spraudņa {name}, nevis no tīmekļa pasta. Ievadiet paroli tikai tad, ja uzticaties šim spraudnim." + "password_note": "Šī veidlapa nāk no spraudņa {name}, nevis no tīmekļa pasta. Ievadiet paroli tikai tad, ja uzticaties šim spraudnim.", + "toast_auto_disabled": "{name} tika izslēgts, jo atkārtoti neizdevās." } }, "global_search": { diff --git a/locales/mn/common.json b/locales/mn/common.json index 9ba13c1cd..97b68f571 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Таны администратор хэрэглэгчийн суулгасан залгаасуудыг унтраасан тул энэ залгаас ажиллахгүй.", "error_approval_unavailable": "Энэ залгаасыг зөвшөөрөл шаарддаг болохоос өмнө суулгасан. Зөвшөөрөлд илгээхийн тулд устгаад дахин суулгана уу.", "error_approval_denied": "Таны администратор энэ залгаасыг зөвшөөрөөгүй.", - "error_approval_pending": "Таны администратор энэ залгаасыг зөвшөөрөхийг хүлээж байна." + "error_approval_pending": "Таны администратор энэ залгаасыг зөвшөөрөхийг хүлээж байна.", + "error_auto_disabled": "Дахин дахин алдаа гаргасан тул унтраасан. Дахин оролдохын тулд асаана уу." }, "consent": { "title": "{name} залгаасад үүнийг зөвшөөрөх үү?", @@ -4250,7 +4251,8 @@ "cancel": "Цуцлах", "close": "Хаах", "turn_off": "{name}-г унтраах", - "password_note": "Энэ маягт вэб шуудангаас биш, {name} залгаасаас ирсэн. Энэ залгаасанд итгэдэг бол л нууц үгээ оруулна уу." + "password_note": "Энэ маягт вэб шуудангаас биш, {name} залгаасаас ирсэн. Энэ залгаасанд итгэдэг бол л нууц үгээ оруулна уу.", + "toast_auto_disabled": "{name} дахин дахин алдаа гаргасан тул унтраагдлаа." } }, "global_search": { diff --git a/locales/nb/common.json b/locales/nb/common.json index 0f2eb0706..c042d86d2 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Administratoren din har slått av tillegg som brukere har installert, så dette tillegget kjører ikke.", "error_approval_unavailable": "Dette tillegget ble installert før godkjenning ble påkrevd. Fjern det og installer det på nytt for å sende det til godkjenning.", "error_approval_denied": "Administratoren din godkjente ikke dette tillegget.", - "error_approval_pending": "Venter på at administratoren din skal godkjenne dette tillegget." + "error_approval_pending": "Venter på at administratoren din skal godkjenne dette tillegget.", + "error_auto_disabled": "Slått av fordi det feilet gjentatte ganger. Slå det på igjen for å prøve på nytt." }, "consent": { "title": "Vil du la {name} gjøre dette?", @@ -4250,7 +4251,8 @@ "cancel": "Avbryt", "close": "Lukk", "turn_off": "Slå av {name}", - "password_note": "Dette skjemaet kommer fra tillegget {name}, ikke fra webmailen. Skriv bare inn et passord hvis du stoler på dette tillegget." + "password_note": "Dette skjemaet kommer fra tillegget {name}, ikke fra webmailen. Skriv bare inn et passord hvis du stoler på dette tillegget.", + "toast_auto_disabled": "{name} ble slått av fordi det feilet gjentatte ganger." } }, "global_search": { diff --git a/locales/nl/common.json b/locales/nl/common.json index 40200e00b..4b2213065 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Je beheerder heeft plug-ins die door gebruikers zijn geïnstalleerd uitgezet, dus deze plug-in draait niet.", "error_approval_unavailable": "Deze plug-in is geïnstalleerd voordat goedkeuring verplicht was. Verwijder hem en installeer hem opnieuw om hem ter goedkeuring te sturen.", "error_approval_denied": "Je beheerder heeft deze plug-in niet goedgekeurd.", - "error_approval_pending": "Wacht tot je beheerder deze plug-in goedkeurt." + "error_approval_pending": "Wacht tot je beheerder deze plug-in goedkeurt.", + "error_auto_disabled": "Uitgezet omdat het steeds misging. Zet het weer aan om het opnieuw te proberen." }, "consent": { "title": "{name} dit toestaan?", @@ -4250,7 +4251,8 @@ "cancel": "Annuleren", "close": "Sluiten", "turn_off": "{name} uitzetten", - "password_note": "Dit formulier komt van de plug-in {name}, niet van de webmail. Vul alleen een wachtwoord in als je deze plug-in vertrouwt." + "password_note": "Dit formulier komt van de plug-in {name}, niet van de webmail. Vul alleen een wachtwoord in als je deze plug-in vertrouwt.", + "toast_auto_disabled": "{name} is uitgezet omdat het steeds misging." } }, "global_search": { diff --git a/locales/pl/common.json b/locales/pl/common.json index f280e9026..d396f0356 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Administrator wyłączył wtyczki instalowane przez użytkowników, więc ta wtyczka nie działa.", "error_approval_unavailable": "Ta wtyczka została zainstalowana, zanim wprowadzono zatwierdzanie. Usuń ją i zainstaluj ponownie, aby wysłać ją do zatwierdzenia.", "error_approval_denied": "Administrator nie zatwierdził tej wtyczki.", - "error_approval_pending": "Czeka na zatwierdzenie przez administratora." + "error_approval_pending": "Czeka na zatwierdzenie przez administratora.", + "error_auto_disabled": "Wyłączono, bo wielokrotnie zawodziło. Włącz ponownie, aby spróbować jeszcze raz." }, "consent": { "title": "Zezwolić wtyczce {name} na te działania?", @@ -4250,7 +4251,8 @@ "cancel": "Anuluj", "close": "Zamknij", "turn_off": "Wyłącz {name}", - "password_note": "Ten formularz pochodzi z wtyczki {name}, a nie z poczty. Wpisz hasło tylko wtedy, gdy ufasz tej wtyczce." + "password_note": "Ten formularz pochodzi z wtyczki {name}, a nie z poczty. Wpisz hasło tylko wtedy, gdy ufasz tej wtyczce.", + "toast_auto_disabled": "{name} wyłączono, bo wielokrotnie zawodziło." } }, "global_search": { diff --git a/locales/pt/common.json b/locales/pt/common.json index 398581f46..7cd725830 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Seu administrador desativou os plugins instalados por usuários, por isso este plugin não é executado.", "error_approval_unavailable": "Este plugin foi instalado antes de as aprovações serem exigidas. Remova-o e instale-o novamente para enviá-lo para aprovação.", "error_approval_denied": "Seu administrador não aprovou este plugin.", - "error_approval_pending": "Aguardando o seu administrador aprovar este plugin." + "error_approval_pending": "Aguardando o seu administrador aprovar este plugin.", + "error_auto_disabled": "Desativado porque falhava repetidamente. Ative-o de novo para tentar outra vez." }, "consent": { "title": "Permitir que {name} faça isto?", @@ -4250,7 +4251,8 @@ "cancel": "Cancelar", "close": "Fechar", "turn_off": "Desativar {name}", - "password_note": "Este formulário vem do plugin {name}, não do webmail. Só introduza uma palavra-passe se confiar neste plugin." + "password_note": "Este formulário vem do plugin {name}, não do webmail. Só introduza uma palavra-passe se confiar neste plugin.", + "toast_auto_disabled": "{name} foi desativado porque falhava repetidamente." } }, "global_search": { diff --git a/locales/ro/common.json b/locales/ro/common.json index 0f848a590..c71303c6b 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Administratorul dvs. a dezactivat pluginurile instalate de utilizatori, așa că acest plugin nu rulează.", "error_approval_unavailable": "Acest plugin a fost instalat înainte ca aprobările să fie obligatorii. Eliminați-l și instalați-l din nou pentru a-l trimite spre aprobare.", "error_approval_denied": "Administratorul dvs. nu a aprobat acest plugin.", - "error_approval_pending": "Se așteaptă ca administratorul să aprobe acest plugin." + "error_approval_pending": "Se așteaptă ca administratorul să aprobe acest plugin.", + "error_auto_disabled": "Dezactivat pentru că a eșuat în mod repetat. Activați-l din nou pentru a reîncerca." }, "consent": { "title": "Permiteți ca {name} să facă acest lucru?", @@ -4250,7 +4251,8 @@ "cancel": "Anulează", "close": "Închide", "turn_off": "Dezactivează {name}", - "password_note": "Acest formular vine de la pluginul {name}, nu de la webmail. Introduceți o parolă doar dacă aveți încredere în acest plugin." + "password_note": "Acest formular vine de la pluginul {name}, nu de la webmail. Introduceți o parolă doar dacă aveți încredere în acest plugin.", + "toast_auto_disabled": "{name} a fost dezactivat pentru că a eșuat în mod repetat." } }, "global_search": { diff --git a/locales/ru/common.json b/locales/ru/common.json index 8dd5a1787..4596ec388 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Администратор отключил плагины, установленные пользователями, поэтому этот плагин не работает.", "error_approval_unavailable": "Этот плагин был установлен до того, как появилось одобрение. Удалите и установите его снова, чтобы отправить на одобрение.", "error_approval_denied": "Администратор не одобрил этот плагин.", - "error_approval_pending": "Ожидается одобрение этого плагина администратором." + "error_approval_pending": "Ожидается одобрение этого плагина администратором.", + "error_auto_disabled": "Отключён, потому что постоянно выдавал ошибки. Включите его снова, чтобы попробовать ещё раз." }, "consent": { "title": "Разрешить плагину {name} это?", @@ -4250,7 +4251,8 @@ "cancel": "Отмена", "close": "Закрыть", "turn_off": "Отключить {name}", - "password_note": "Эта форма от плагина {name}, а не от веб-почты. Вводите пароль, только если доверяете этому плагину." + "password_note": "Эта форма от плагина {name}, а не от веб-почты. Вводите пароль, только если доверяете этому плагину.", + "toast_auto_disabled": "{name} отключён, потому что постоянно выдавал ошибки." } }, "global_search": { diff --git a/locales/sk/common.json b/locales/sk/common.json index b7c7f4ecf..22f4c5530 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Váš správca vypol doplnky inštalované používateľmi, preto tento doplnok nebeží.", "error_approval_unavailable": "Tento doplnok bol nainštalovaný skôr, než sa vyžadovalo schválenie. Odstráňte ho a nainštalujte znova, aby sa odoslal na schválenie.", "error_approval_denied": "Váš správca tento doplnok neschválil.", - "error_approval_pending": "Čaká sa, kým správca tento doplnok schváli." + "error_approval_pending": "Čaká sa, kým správca tento doplnok schváli.", + "error_auto_disabled": "Vypnuté, pretože opakovane zlyhávalo. Zapnite ho znova a skúste to znova." }, "consent": { "title": "Povoliť doplnku {name} tieto akcie?", @@ -4250,7 +4251,8 @@ "cancel": "Zrušiť", "close": "Zavrieť", "turn_off": "Vypnúť {name}", - "password_note": "Tento formulár pochádza z doplnku {name}, nie z webmailu. Heslo zadajte, len ak tomuto doplnku dôverujete." + "password_note": "Tento formulár pochádza z doplnku {name}, nie z webmailu. Heslo zadajte, len ak tomuto doplnku dôverujete.", + "toast_auto_disabled": "{name} bol vypnutý, pretože opakovane zlyhával." } }, "global_search": { diff --git a/locales/tr/common.json b/locales/tr/common.json index c92a95395..acf237629 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Yöneticiniz kullanıcıların yüklediği eklentileri kapattı, bu yüzden bu eklenti çalışmıyor.", "error_approval_unavailable": "Bu eklenti, onay zorunlu olmadan önce yüklendi. Onaya göndermek için kaldırıp yeniden yükleyin.", "error_approval_denied": "Yöneticiniz bu eklentiyi onaylamadı.", - "error_approval_pending": "Bu eklentinin yöneticiniz tarafından onaylanması bekleniyor." + "error_approval_pending": "Bu eklentinin yöneticiniz tarafından onaylanması bekleniyor.", + "error_auto_disabled": "Sürekli hata verdiği için kapatıldı. Yeniden denemek için tekrar açın." }, "consent": { "title": "{name} eklentisine bu izin verilsin mi?", @@ -4250,7 +4251,8 @@ "cancel": "İptal", "close": "Kapat", "turn_off": "{name} eklentisini kapat", - "password_note": "Bu form web postasından değil, {name} eklentisinden geliyor. Yalnızca bu eklentiye güveniyorsanız parola girin." + "password_note": "Bu form web postasından değil, {name} eklentisinden geliyor. Yalnızca bu eklentiye güveniyorsanız parola girin.", + "toast_auto_disabled": "{name} sürekli hata verdiği için kapatıldı." } }, "global_search": { diff --git a/locales/uk/common.json b/locales/uk/common.json index a8ce891cb..8ce2a0b12 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "Ваш адміністратор вимкнув плагіни, встановлені користувачами, тому цей плагін не працює.", "error_approval_unavailable": "Цей плагін встановлено ще до того, як з'явилося схвалення. Видаліть і встановіть його знову, щоб надіслати на схвалення.", "error_approval_denied": "Ваш адміністратор не схвалив цей плагін.", - "error_approval_pending": "Очікується, що ваш адміністратор схвалить цей плагін." + "error_approval_pending": "Очікується, що ваш адміністратор схвалить цей плагін.", + "error_auto_disabled": "Вимкнено, бо постійно виникали помилки. Увімкніть його знову, щоб спробувати ще раз." }, "consent": { "title": "Дозволити плагіну {name} це?", @@ -4250,7 +4251,8 @@ "cancel": "Скасувати", "close": "Закрити", "turn_off": "Вимкнути {name}", - "password_note": "Ця форма від плагіна {name}, а не від вебпошти. Вводьте пароль, лише якщо довіряєте цьому плагіну." + "password_note": "Ця форма від плагіна {name}, а не від вебпошти. Вводьте пароль, лише якщо довіряєте цьому плагіну.", + "toast_auto_disabled": "{name} вимкнено, бо постійно виникали помилки." } }, "global_search": { diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index c62928181..1415806e0 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "管理員已關閉使用者安裝的外掛程式,因此此外掛程式不會執行。", "error_approval_unavailable": "此外掛程式是在需要核准之前安裝的。請移除後重新安裝,以送交核准。", "error_approval_denied": "管理員未核准此外掛程式。", - "error_approval_pending": "正在等待管理員核准此外掛程式。" + "error_approval_pending": "正在等待管理員核准此外掛程式。", + "error_auto_disabled": "由於反覆出錯已關閉。重新開啟即可再試一次。" }, "consent": { "title": "要允許 {name} 執行這些動作嗎?", @@ -4250,7 +4251,8 @@ "cancel": "取消", "close": "關閉", "turn_off": "關閉 {name}", - "password_note": "此表單來自外掛程式 {name},而不是網頁郵件。只有在您信任此外掛程式時才輸入密碼。" + "password_note": "此表單來自外掛程式 {name},而不是網頁郵件。只有在您信任此外掛程式時才輸入密碼。", + "toast_auto_disabled": "{name} 因反覆出錯已被關閉。" } }, "global_search": { diff --git a/locales/zh/common.json b/locales/zh/common.json index eb74276ae..10342d03d 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -4016,7 +4016,8 @@ "error_user_plugins_off": "管理员已关闭用户安装的插件,因此此插件不会运行。", "error_approval_unavailable": "此插件是在需要审批之前安装的。请移除后重新安装,以提交审批。", "error_approval_denied": "管理员未批准此插件。", - "error_approval_pending": "正在等待管理员批准此插件。" + "error_approval_pending": "正在等待管理员批准此插件。", + "error_auto_disabled": "由于反复出错已关闭。重新开启即可再试一次。" }, "consent": { "title": "允许 {name} 执行这些操作?", @@ -4250,7 +4251,8 @@ "cancel": "取消", "close": "关闭", "turn_off": "关闭 {name}", - "password_note": "此表单来自插件 {name},而不是网页邮箱。只有在您信任此插件时才输入密码。" + "password_note": "此表单来自插件 {name},而不是网页邮箱。只有在您信任此插件时才输入密码。", + "toast_auto_disabled": "{name} 因反复出错已被关闭。" } }, "global_search": { diff --git a/stores/plugin-store.ts b/stores/plugin-store.ts index 3917a5358..fede3c8ee 100644 --- a/stores/plugin-store.ts +++ b/stores/plugin-store.ts @@ -207,7 +207,7 @@ export const usePluginStore = create()( } // Ensure bridge is wired before loading (may not have run initializePlugins yet) - setPluginStoreAccessor({ setPluginStatus: get().setPluginStatus }); + wireLoader(get, set); set(state => ({ plugins: state.plugins.map(p => @@ -283,9 +283,7 @@ export const usePluginStore = create()( } // Wire up bridges - setPluginStoreAccessor({ - setPluginStatus: get().setPluginStatus, - }); + wireLoader(get, set); setupAutoDisable(); // Keep the sandbox locale in step with the app locale. Set it // synchronously *before* activation so background instances get the @@ -355,6 +353,23 @@ export const usePluginStore = create()( ) ); +/** Give the loader its way back into the store. */ +function wireLoader( + get: () => PluginStoreState, + set: (partial: Partial | ((state: PluginStoreState) => Partial)) => void, +): void { + setPluginStoreAccessor({ + setPluginStatus: get().setPluginStatus, + markAutoDisabled: (id) => set(state => ({ + plugins: state.plugins.map(p => (p.id === id + // enabled: false is persisted, so a reload doesn't start the + // failing plugin again behind the user's back. + ? { ...p, enabled: false, status: 'error' as PluginStatus, error: 'auto-disabled' } + : p)), + })), + }); +} + // ─── Server Plugin Sync ────────────────────────────────────── interface ServerPluginInfo {