feat(frontend): système de plugins — runtime, menus, layouts custom (#43)

Lots F1-F4 : runtime de chargement/registre plugin, extension des menus
existants, panneau de gestion des plugins, types de layout custom
(sélecteur, fallback, cellule dédiée) branchés sur le port plugin.
Suite npm typecheck/test verte (947/947).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 07:37:11 +02:00
parent bb35641715
commit ac726d075e
41 changed files with 3245 additions and 24 deletions

View File

@ -0,0 +1,73 @@
/**
* `PluginLayoutCellView` — renders a `customPluginLayout` top-level
* {@link LayoutNode} (ticket #43, F4, carnet v2 §3): the plugin's registered
* React component when its provider is loaded and declares the type,
* otherwise {@link PluginLayoutFallback}.
*
* State is opaque to the domain (`unknown`) and round-trips through
* `LayoutGateway` exactly like any other node field — `setState` here is the
* only way a plugin component is meant to persist it (never direct gateway
* calls from inside a plugin component).
*/
import type { CustomPluginLayoutCell, PluginAdmin } from "@/domain";
import { useGateways } from "@/app/di";
import { usePluginRuntime } from "./PluginRuntimeProvider";
import { PluginLayoutFallback } from "./PluginLayoutFallback";
import { resolvePluginLayoutAvailability } from "./layoutAvailability";
interface PluginLayoutCellViewProps {
projectId: string;
cell: CustomPluginLayoutCell;
installedPlugins?: PluginAdmin[];
onStateChange: (nextState: unknown) => void;
onOpenPlugins: () => void;
onChooseAnotherLayout: () => void;
}
export function PluginLayoutCellView({
projectId,
cell,
installedPlugins,
onStateChange,
onOpenPlugins,
onChooseAnotherLayout,
}: PluginLayoutCellViewProps) {
const { registry } = usePluginRuntime();
const gateways = useGateways();
const availability = resolvePluginLayoutAvailability(registry, cell, installedPlugins);
if (availability !== "available") {
const providerDisplayName =
registry.get(cell.pluginId)?.displayName ??
installedPlugins?.find((p) => p.id === cell.pluginId)?.displayName;
return (
<PluginLayoutFallback
cell={cell}
availability={availability}
providerDisplayName={providerDisplayName}
onOpenPlugins={onOpenPlugins}
onChooseAnotherLayout={onChooseAnotherLayout}
/>
);
}
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,
}}
/>
);
}