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

@ -837,11 +837,18 @@ export interface GridContainer {
/**
* A node in the layout tree. Tagged on `type` with the payload under `node`,
* matching the backend `#[serde(tag = "type", content = "node")]`.
*
* `customPluginLayout` (#43, F4, carnet v2 §3) is a true top-level variant —
* a plugin-provided layout occupies a slot in the tree at the same conceptual
* level as a terminal leaf, a split or a grid, never a field bolted onto
* `LeafCell` (that shape was tried and explicitly retired by Architect after
* QA flagged the ambiguity — see `CustomPluginLayoutCell`'s doc comment).
*/
export type LayoutNode =
| { type: "leaf"; node: LeafCell }
| { type: "split"; node: SplitContainer }
| { type: "grid"; node: GridContainer };
| { type: "grid"; node: GridContainer }
| { type: "customPluginLayout"; node: CustomPluginLayoutCell };
/** The root of a tab's terminal layout. */
export interface LayoutTree {
@ -1448,3 +1455,190 @@ export interface PairingCode {
export function normalizePairingCode(raw: string): string {
return raw.replace(/[\s-]+/g, "").toUpperCase();
}
// ---------------------------------------------------------------------------
// Plugin system (ticket #43) — mirrors of the backend DTOs described in the
// carnet §§2, 5, 6, 7. Full-trust, locally-installed, globally-scoped plugins
// contributing menus/menu-items/layouts/MCP servers via a pre-compiled ESM
// bundle loaded at bootstrap. See `@/plugins/runtime` for the loader/registry
// and `@/features/plugins` for the admin surface + menu/layout integration.
// ---------------------------------------------------------------------------
/** Where an installed plugin package came from (admin display only). */
export type PluginSourceKind = "archive" | "directory";
/** Persisted lifecycle state of an installed plugin (carnet §1.4). */
export type PluginLifecycleState =
| "enabled"
| "disabled"
| "pending-enable"
| "pending-disable"
| "pending-uninstall"
| "invalid";
/** Counts of what a plugin declares, shown in the admin list (carnet §5). */
export interface PluginContributionSummary {
topLevelMenus: number;
menuItems: number;
layouts: number;
mcpServers: number;
}
/** One installed plugin, as shown in `Paramètres > Plugins` (mirror of `PluginAdminDto`). */
export interface PluginAdmin {
id: string;
displayName: string;
publisher?: string;
version: string;
description?: string;
iconUrl?: string;
sourceKind: PluginSourceKind;
sourceLabel?: string;
lifecycleState: PluginLifecycleState;
enabled: boolean;
pendingEnableState?: boolean;
pendingUninstall: boolean;
restartRequired: boolean;
trustLevel: "full";
contributionSummary: PluginContributionSummary;
error?: string;
}
/** A manifest validation issue surfaced during pre-install review. */
export interface PluginReviewIssue {
severity: "error" | "warning";
message: string;
}
/** Pre-install review of a candidate package (carnet §4, `ReviewPluginPackage`). */
export interface PluginReview {
id: string;
displayName: string;
publisher?: string;
version: string;
description?: string;
trustLevel: "full";
contributionSummary: PluginContributionSummary;
issues: PluginReviewIssue[];
/** False when an `error`-severity issue makes install unsafe/impossible. */
installable: boolean;
}
/** Outcome of `install_from_archive` / `install_from_directory`. */
export interface PluginInstallResult {
plugin: PluginAdmin;
restartRequired: boolean;
}
/** Outcome of `uninstall`. */
export interface PluginUninstallResult {
pluginId: string;
restartRequired: boolean;
}
/** Contribution declarations for one plugin, as consumed by the runtime loader. */
export interface PluginContributionDto {
menus: PluginTopLevelMenuContribution[];
menuItems: PluginMenuItemContribution[];
layouts: PluginLayoutContribution[];
mcpServers: PluginMcpServerSummary[];
}
/** One entry of `plugin_list_runtime_contributions` (mirror of `PluginRuntimePluginDto`). */
export interface PluginRuntimePlugin {
id: string;
displayName: string;
publisher?: string;
version: string;
bundleUrl: string;
iconUrl?: string;
contentHash: string;
contributes: PluginContributionDto;
}
/** Bootstrap catalog fetched once, before loading any plugin bundle. */
export interface PluginRuntimeContributionCatalog {
plugins: PluginRuntimePlugin[];
}
/** Manifest declaration of a top-level menu (carnet §7.1). */
export interface PluginTopLevelMenuContribution {
id: string;
label: string;
topLevel: true;
order?: number;
icon?: string;
}
/** Native + plugin menu ids an item can target (carnet §7.2). */
export type MenuTargetId = "panels" | "settings" | `plugin:${string}`;
/** Manifest declaration of a menu item contributed into an existing/plugin menu. */
export interface PluginMenuItemContribution {
id: string;
targetMenuId: MenuTargetId;
label: string;
command: string;
order?: number;
icon?: string;
when?: string;
}
/** A menu item resolved for rendering — enablement already evaluated (carnet §7.2). */
export interface ResolvedPluginMenuItem {
id: string;
pluginId: string;
pluginDisplayName: string;
targetMenuId: MenuTargetId;
label: string;
command: string;
enabled: boolean;
disabledReason?: string;
groupLabel?: string;
order: number;
iconUrl?: string;
}
/** Manifest declaration of a custom React layout (carnet §7.3). */
export interface PluginLayoutContribution {
type: string;
label: string;
component: string;
order?: number;
icon?: string;
when?: string;
}
/** Whether a persisted plugin layout can currently be rendered by its provider. */
export type PluginLayoutAvailability =
| "available"
| "plugin-disabled"
| "plugin-missing"
| "incompatible";
/**
* Opaque, domain-persisted identity + state of a `customPluginLayout`
* top-level {@link LayoutNode} variant (#43, carnet v2 §3.2 — the canonical
* schema, mirroring the backend `CustomPluginLayoutCell` exactly: `id`,
* `pluginId`, `layoutType`, `state`, camelCase). No display name travels on
* the wire — the UI derives it from the plugin runtime registry/admin list at
* render time, never persists it here.
*
* A previous frontend-only shape nested this under `LeafCell.pluginLayout`
* with a `kind`/`nodeId`/`providerPluginId` field naming; that shape was
* never part of the IPC/persistence contract and has been retired — do not
* reintroduce it.
*/
export interface CustomPluginLayoutCell {
id: string;
pluginId: string;
layoutType: string;
state: unknown;
}
/** Manifest declaration of an MCP server the plugin supervises (carnet §7.4, summary only). */
export interface PluginMcpServerSummary {
id: string;
displayName: string;
autoStart?: boolean;
}