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

@ -38,6 +38,7 @@ import {
TargetAnnouncementsOverlay,
useTargetAnnouncements,
} from "@/features/announcements";
import { PluginLayoutCellView } from "@/features/plugins";
import {
modelServerOverlayText,
describeModelServerDownload,
@ -79,9 +80,21 @@ interface LayoutGridProps {
layoutId?: string;
/** Opens the read-only canonical transcript for a conversation. */
onOpenConversation?: (conversationId: string) => void;
/**
* Navigates to `Paramètres > Plugins` (#43, F4) — the "Ouvrir Plugins" action
* of a plugin layout's unavailable fallback. Threaded down to
* `PluginLayoutCellView` the same way `onOpenConversation` is.
*/
onOpenPluginsSettings?: () => void;
}
export function LayoutGrid({ projectId, cwd, layoutId, onOpenConversation }: LayoutGridProps) {
export function LayoutGrid({
projectId,
cwd,
layoutId,
onOpenConversation,
onOpenPluginsSettings,
}: LayoutGridProps) {
const vm = useLayout(projectId, layoutId);
const work = useProjectWorkState(projectId);
@ -117,6 +130,7 @@ export function LayoutGrid({ projectId, cwd, layoutId, onOpenConversation }: Lay
workState={work.state}
refreshWorkState={work.refresh}
onOpenConversation={onOpenConversation}
onOpenPluginsSettings={onOpenPluginsSettings}
/>
</div>
);
@ -132,6 +146,7 @@ interface NodeViewProps {
workState: ProjectWorkState | null;
refreshWorkState: () => Promise<void>;
onOpenConversation?: (conversationId: string) => void;
onOpenPluginsSettings?: () => void;
}
function NodeView({
@ -143,8 +158,24 @@ function NodeView({
workState,
refreshWorkState,
onOpenConversation,
onOpenPluginsSettings,
}: NodeViewProps) {
switch (node.type) {
case "customPluginLayout":
// A true top-level `LayoutNode` variant (#43, F4, carnet v2 §3) — a
// plugin layout occupies a slot in the tree at the same level as a
// terminal leaf, split or grid. Rendered separately from `LeafView`
// (which owns a lot of terminal-only concerns — write-portal,
// model-server overlay, agent dropdown — none of which apply here).
return (
<PluginLayoutCellView
projectId={projectId}
cell={node.node}
onStateChange={(nextState) => vm.setPluginLayoutState(node.node.id, nextState)}
onOpenPlugins={() => onOpenPluginsSettings?.()}
onChooseAnotherLayout={() => vm.replacePluginLayoutWithTerminal(node.node.id)}
/>
);
case "leaf":
return (
<LeafView
@ -172,6 +203,7 @@ function NodeView({
workState={workState}
refreshWorkState={refreshWorkState}
onOpenConversation={onOpenConversation}
onOpenPluginsSettings={onOpenPluginsSettings}
/>
);
case "grid":
@ -184,6 +216,7 @@ function NodeView({
workState={workState}
refreshWorkState={refreshWorkState}
onOpenConversation={onOpenConversation}
onOpenPluginsSettings={onOpenPluginsSettings}
/>
);
}
@ -1153,6 +1186,7 @@ interface SplitViewProps {
workState: ProjectWorkState | null;
refreshWorkState: () => Promise<void>;
onOpenConversation?: (conversationId: string) => void;
onOpenPluginsSettings?: () => void;
}
function SplitView({
@ -1163,6 +1197,7 @@ function SplitView({
workState,
refreshWorkState,
onOpenConversation,
onOpenPluginsSettings,
}: SplitViewProps) {
const isRow = split.direction === "row";
const baseWeights = split.children.map((c) => c.weight);
@ -1207,6 +1242,7 @@ function SplitView({
workState={workState}
refreshWorkState={refreshWorkState}
onOpenConversation={onOpenConversation}
onOpenPluginsSettings={onOpenPluginsSettings}
parentSplit={{
container: split.id,
index: i,
@ -1301,6 +1337,7 @@ interface GridViewProps {
workState: ProjectWorkState | null;
refreshWorkState: () => Promise<void>;
onOpenConversation?: (conversationId: string) => void;
onOpenPluginsSettings?: () => void;
}
function GridView({
@ -1311,6 +1348,7 @@ function GridView({
workState,
refreshWorkState,
onOpenConversation,
onOpenPluginsSettings,
}: GridViewProps) {
const cols = normalizeWeights(grid.colWeights)
.map((p) => `${p}fr`)
@ -1351,6 +1389,7 @@ function GridView({
workState={workState}
refreshWorkState={refreshWorkState}
onOpenConversation={onOpenConversation}
onOpenPluginsSettings={onOpenPluginsSettings}
/>
</div>
))}