fix(plugins): isole les contributions plugin en erreur et durcit menus.ts

Le chargement de l'archive hello-plugin (build/hello-plugin-0.1.0.zip)
vidait la fenêtre principale : une contribution plugin fautive
remontait jusqu'au rendu global au lieu de rester locale à la cellule.
Ajoute un boundary local dans PluginLayoutCellView/PluginLayoutSelectorSection
et durcit menus.ts/loader.ts/registry.ts contre les entrées de menu ou
contributions malformées.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 09:08:14 +02:00
parent eec3e0a5bc
commit aa850374ce
9 changed files with 316 additions and 58 deletions

View File

@ -10,6 +10,8 @@
* calls from inside a plugin component).
*/
import { Component, type ErrorInfo, type ReactNode } from "react";
import type { CustomPluginLayoutCell, PluginAdmin } from "@/domain";
import { useGateways } from "@/app/di";
import { usePluginRuntime } from "./PluginRuntimeProvider";
@ -25,6 +27,38 @@ interface PluginLayoutCellViewProps {
onChooseAnotherLayout: () => void;
}
interface PluginLayoutErrorBoundaryProps {
resetKey: string;
fallback: ReactNode;
children: ReactNode;
}
class PluginLayoutErrorBoundary extends Component<
PluginLayoutErrorBoundaryProps,
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError(): { hasError: boolean } {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error("[plugin-layout] render failed", error, errorInfo);
}
componentDidUpdate(prevProps: PluginLayoutErrorBoundaryProps): void {
if (prevProps.resetKey !== this.props.resetKey && this.state.hasError) {
this.setState({ hasError: false });
}
}
render(): ReactNode {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}
export function PluginLayoutCellView({
projectId,
cell,
@ -36,11 +70,11 @@ export function PluginLayoutCellView({
const { registry } = usePluginRuntime();
const gateways = useGateways();
const availability = resolvePluginLayoutAvailability(registry, cell, installedPlugins);
const providerDisplayName =
registry.get(cell.pluginId)?.displayName ??
installedPlugins?.find((p) => p.id === cell.pluginId)?.displayName;
if (availability !== "available") {
const providerDisplayName =
registry.get(cell.pluginId)?.displayName ??
installedPlugins?.find((p) => p.id === cell.pluginId)?.displayName;
return (
<PluginLayoutFallback
cell={cell}
@ -53,21 +87,35 @@ export function PluginLayoutCellView({
}
const Component = registry.layoutComponent(cell.pluginId, cell.layoutType)!;
return (
<Component
projectId={projectId}
nodeId={cell.id}
layoutType={cell.layoutType}
state={cell.state}
setState={onStateChange}
availability="available"
gateways={{
project: gateways.project,
git: gateways.git,
terminal: gateways.terminal,
agents: gateways.agent,
system: gateways.system,
}}
const fallback = (
<PluginLayoutFallback
cell={cell}
availability="incompatible"
providerDisplayName={providerDisplayName}
onOpenPlugins={onOpenPlugins}
onChooseAnotherLayout={onChooseAnotherLayout}
/>
);
return (
<PluginLayoutErrorBoundary
resetKey={`${cell.pluginId}:${cell.layoutType}:${cell.id}`}
fallback={fallback}
>
<Component
projectId={projectId}
nodeId={cell.id}
layoutType={cell.layoutType}
state={cell.state}
setState={onStateChange}
availability="available"
gateways={{
project: gateways.project,
git: gateways.git,
terminal: gateways.terminal,
agents: gateways.agent,
system: gateways.system,
}}
/>
</PluginLayoutErrorBoundary>
);
}