diff --git a/frontend/src/features/plugins/PluginRuntimeProvider.tsx b/frontend/src/features/plugins/PluginRuntimeProvider.tsx index 026ee17..2ff26b5 100644 --- a/frontend/src/features/plugins/PluginRuntimeProvider.tsx +++ b/frontend/src/features/plugins/PluginRuntimeProvider.tsx @@ -38,6 +38,13 @@ const EMPTY_PLUGIN_RUNTIME: PluginRuntimeContextValue = { const PluginRuntimeContext = createContext(EMPTY_PLUGIN_RUNTIME); +function describeError(e: unknown): string { + if (e && typeof e === "object" && "message" in e) { + return String((e as { message: unknown }).message); + } + return String(e); +} + interface PluginRuntimeProviderProps { children: ReactNode; /** Test/Storybook escape hatch — skips the gateway fetch and uses this value as-is. */ @@ -72,10 +79,19 @@ export function PluginRuntimeProvider({ children, value: injected }: PluginRunti if (cancelled) return; setValue({ registry: result.registry, failures: result.failures, loading: false }); }) - .catch(() => { + .catch((e: unknown) => { // No plugin gateway / catalog fetch failed: run with zero plugins // rather than blocking the app (full-trust plugins are additive). - if (!cancelled) setValue((prev) => ({ ...prev, loading: false })); + if (!cancelled) { + setValue((prev) => ({ + ...prev, + failures: [ + ...prev.failures, + { pluginId: "", reason: describeError(e) }, + ], + loading: false, + })); + } }); return () => { cancelled = true; diff --git a/frontend/src/features/plugins/PluginsPanel.tsx b/frontend/src/features/plugins/PluginsPanel.tsx index 35e3d7f..6b1c6e4 100644 --- a/frontend/src/features/plugins/PluginsPanel.tsx +++ b/frontend/src/features/plugins/PluginsPanel.tsx @@ -167,7 +167,11 @@ export function PluginsPanel() { )}
- {p.enabled ? ( + {p.lifecycleState === "invalid" ? ( + + ) : p.enabled ? (