--- issueRef: "#43" version: 6 updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} updatedAt: 1784707900405 --- # Ticket #43 — Cadrage consolidé v2 du système de plugins > Mise à jour Architect du 2026-07-21 après retour QA F4. Ce carnet v2 annule les ambiguïtés du cadrage initial, en particulier sur le schéma de layout custom plugin. ## 0. Décisions discovery conservées - Plugins **full-trust** v1 : pas de sandbox UI. - Bundle **JS/ESM pré-compilé**, importé dynamiquement ; IdeA ne compile pas de TS/TSX. - Installation **globale** dans le dossier données utilisateur de l'application, pas dans les projets. - Distribution v1 locale : dossier ou archive locale ; packaging compatible archive partageable future type `.vsix`. - Contributions v1 : menus top-level, items de menus existants, layouts custom React arbitraires, serveurs MCP externes. - MCP plugin = process serveur MCP externe déclaré par manifeste et supervisé via le pont MCP existant. - Propreté retrait : après uninstall + redémarrage, aucune contribution, aucun process, aucune entrée fantôme, aucun état plugin-specific. ## 1. Store global et cycle de vie Store global : ```text {app_data_dir}/plugins/ registry.json installed/ / idea-plugin.json dist/index.js assets/... servers/... ``` États v1 persistés : ```text enabled disabled pending-enable pending-disable pending-uninstall invalid ``` Règles : - Installer depuis archive ou dossier local copie toujours un snapshot dans `installed//`. - `registry.json` porte seulement l'état global nécessaire ; une désinstallation réussie supprime l'entrée registry. - Disable/uninstall en session masque les contributions UI et arrête MCP immédiatement si possible, mais le code ESM déjà importé n'est purgé strictement qu'au redémarrage. - Au boot, le frontend reconstruit une registry plugin vide puis charge uniquement les plugins `enabled` et non `pending-uninstall`. ## 2. Manifeste v1 Fichier obligatoire : `idea-plugin.json`. ```json { "ideaPluginManifestVersion": 1, "id": "dev.acme.gitgraph", "displayName": "Git Graph", "publisher": "Acme DevTools", "version": "1.2.3", "engines": { "idea": ">=0.1.0 <1.0.0" }, "main": "dist/index.js", "icon": "assets/icon.svg", "trustLevel": "full", "activationScope": "app", "capabilities": ["ui", "mcp"], "contributes": { "menus": [], "menuItems": [], "layouts": [], "mcpServers": [] }, "archive": { "files": ["idea-plugin.json", "dist/**", "assets/**", "servers/**"] } } ``` Validation : - `trustLevel` vaut uniquement `full` en v1. - `activationScope` est optionnel, vaut `app` par défaut, ou `project` pour différer l'activation runtime jusqu'au premier projet focused ; l'état `pending` n'est pas une erreur de chargement. - `main`, `icon`, assets et commandes MCP relatives ne doivent contenir ni chemin absolu ni `..`. - `id` stable, unique, reverse-DNS recommandé. - `version` SemVer. - `engines.idea` incompatible => plugin `invalid`, jamais chargé. ## 3. Contrat définitif des layouts custom plugin ### 3.1 Décision ferme `CustomPluginLayout` est une **vraie variante top-level de `LayoutNode`**, pas un champ optionnel de `LeafCell`. Motif : un layout plugin occupe une zone de l'arbre au même niveau conceptuel qu'une leaf terminal, un split ou une grid. Le mettre dans `LeafCell` mélangerait deux responsabilités incompatibles : terminal/session/agent d'un côté, composant React arbitraire et état opaque plugin de l'autre. ### 3.2 Schéma JSON canonique Le layout tree garde le format serde existant `#[serde(tag = "type", content = "node")]`. Union canonique : ```ts export type LayoutNode = | { type: "leaf"; node: LeafCell } | { type: "split"; node: SplitContainer } | { type: "grid"; node: GridContainer } | { type: "customPluginLayout"; node: CustomPluginLayoutCell }; ``` Payload canonique : ```ts export interface CustomPluginLayoutCell { id: string; pluginId: string; layoutType: string; state: unknown; } ``` Exemple complet : ```json { "root": { "type": "customPluginLayout", "node": { "id": "018f0c5a-2b4b-70d4-a7c2-300000000001", "pluginId": "dev.acme.gitgraph", "layoutType": "dev.acme.gitgraph.layout", "state": { "branchFilter": "main" } } } } ``` Dans un split : ```json { "type": "split", "node": { "id": "split-1", "direction": "row", "children": [ { "weight": 1, "node": { "type": "leaf", "node": { "id": "terminal-1" } } }, { "weight": 1, "node": { "type": "customPluginLayout", "node": { "id": "plugin-cell-1", "pluginId": "dev.acme.gitgraph", "layoutType": "dev.acme.gitgraph.layout", "state": {} } } } ] } } ``` Dans une grid, `GridCell.node` peut pareillement être `{ type: "customPluginLayout", node: ... }`. ### 3.3 Ce qui est explicitement interdit Cette forme n'est **pas** contractuelle et ne doit plus être produite ni consommée comme modèle canonique : ```json { "type": "leaf", "node": { "id": "leaf-1", "pluginLayout": { "pluginId": "dev.acme.gitgraph", "layoutType": "dev.acme.gitgraph.layout", "state": {} } } } ``` `LeafCell.pluginLayout` est une divergence frontend issue de l'ambiguïté initiale. Elle doit être supprimée du modèle domaine TS ou limitée à une migration locale temporaire de tests/mocks ; elle ne fait pas partie de l'IPC ni de la persistance. ### 3.4 Disponibilité et fallback La disponibilité n'est pas stockée dans le layout. Elle est dérivée côté UI à partir du plugin registry/runtime catalog : ```ts export type PluginLayoutAvailability = | "available" | "plugin-disabled" | "plugin-missing" | "incompatible"; ``` Rendu : - `available` : rendre le composant React enregistré pour `layoutType`. - `plugin-disabled`, `plugin-missing`, `incompatible` : rendre le fallback non destructif `Layout indisponible`. - Le fallback ne transforme pas automatiquement le nœud et ne supprime jamais `state`. ### 3.5 Ajustements requis Verdict convergence F4 : **frontend à ajuster, backend à conserver**. Backend : - L'implémentation actuelle `LayoutNode::CustomPluginLayout(CustomPluginLayoutCell)` sérialisée `type: "customPluginLayout"` est le contrat canonique. - À vérifier seulement : roundtrip serde et DTO Tauri exposent bien `pluginId`, `layoutType`, `state` en camelCase. Frontend : - Ajouter la variante `{ type: "customPluginLayout"; node: CustomPluginLayoutCell }` à `LayoutNode`. - Retirer `pluginLayout?: CustomPluginLayoutCell` de `LeafCell` comme contrat domaine. - Adapter `LayoutGrid`/renderer récursif pour router `type === "customPluginLayout"` vers `PluginLayoutCellView`. - Adapter `layoutAvailability`, fallback et tests pour recevoir le payload depuis `node.node` de la variante top-level. - Ajouter un test de parsing/rendu avec un layout JSON produit par Rust contenant `type: "customPluginLayout"`. ## 4. Contribution points menus et `when` Menus v1 : ```ts type MenuTargetId = "panels" | "settings" | `plugin:${string}`; ``` Items v1 : ```ts interface PluginMenuItemContribution { id: string; targetMenuId: MenuTargetId; label: string; command: string; order?: number; icon?: string; when?: string; } ``` `when` v1 utilise le mini-langage booléen `&&`, `||`, `!`, parenthèses. Variables : ```ts type WhenVariable = | "projectOpen" | "gitRepository" | "agentSelected" | "terminalFocused" | "layoutCellFocused"; ``` Arbitrage QA sur les variables actuellement figées à `false` : - `projectOpen` : obligatoire v1, doit refléter l'état réel. - `gitRepository` : **bloquant avant clôture #43/F3**. Le manifeste exemple et le cas GitGraph dépendent de `projectOpen && gitRepository`; le laisser à `false` rend des contributions valides inatteignables. - `agentSelected`, `terminalFocused`, `layoutCellFocused` : dette acceptable v1 si elles restent explicitement documentées comme **best-effort non câblé** et donc `false` jusqu'à un lot focus/selection dédié. Ce n'est pas bloquant pour F4 ni pour la clean-removal QA, sauf si un plugin de validation les utilise dans son manifeste. Ajustement recommandé : DevFrontend câble au minimum `gitRepository` depuis l'état projet/git déjà disponible, ou retire temporairement `gitRepository` des manifests/tests de validation. La préférence architecture est de le câbler, car il est déjà annoncé comme variable v1. ## 5. Contribution MCP et `${appDataDir}` Manifest MCP : ```ts interface PluginMcpServerContribution { id: string; displayName: string; command: string; args?: string[]; env?: Record; cwd?: "${pluginRoot}" | "${appDataDir}" | string; transport: "stdio"; autoStart?: boolean; } ``` Variables de substitution contractuelles dans `command`, `args`, `env` et `cwd` : - `${pluginRoot}` : obligatoire v1. - `${appDataDir}` : **obligatoire v1 si la spec continue de l'accepter**. - `${projectRoot}` : interdit v1. Arbitrage QA sur `${appDataDir}` non implémenté : - Pas bloquant pour F4 layout. - **Bloquant pour clôture B4/#43** si le manifeste continue de documenter `${appDataDir}` comme disponible. Un contrat annoncé mais non substitué crée des specs MCP fausses. - Deux sorties acceptables, par ordre de préférence : 1. DevBackend implémente la substitution `${appDataDir}` dans les specs MCP plugin et ajoute tests args/env/cwd. 2. Si le coût est refusé pour v1, retirer `${appDataDir}` du contrat manifeste et du validator, puis documenter explicitement `${pluginRoot}` comme seule variable v1. Décision architecture par défaut : garder `${appDataDir}` et le faire implémenter par DevBackend, car le store global est déjà résolu côté backend et le coût est borné. ## 6. Lots et responsabilités actualisés ### F4 — convergence layout custom plugin Owner : DevFrontend. Livrables : - Union TS `LayoutNode` alignée sur Rust avec `customPluginLayout` top-level. - Renderer/fallback branchés sur cette variante. - Suppression du contrat `LeafCell.pluginLayout`. - Test frontend à partir d'un JSON Rust réel. Backend F4 : pas de refonte attendue ; seulement vérifier/maintenir les tests serde existants. ### F3 — `when.gitRepository` Owner : DevFrontend. Livrable : `gitRepository` ne doit plus être figé à `false` pour un projet Git réel avant clôture #43/F3. ### B4 — `${appDataDir}` MCP Owner : DevBackend. Livrable : substitution `${appDataDir}` dans `command`, `args`, `env`, `cwd` des specs MCP plugin, ou retrait explicite du contrat si arbitré à la baisse. Par défaut : implémenter. ### QA — non-régression à ajouter - Layout JSON backend `customPluginLayout` top-level rendu côté frontend. - Fallback atteint si plugin missing/disabled avec nœud top-level. - Aucun fallback basé sur `leaf.node.pluginLayout` ne compte comme validation du contrat. - MCP spec avec `${pluginRoot}` et `${appDataDir}`. - Menu item `when: "projectOpen && gitRepository"` actif dans un projet Git. ## 7. Non-objectifs maintenus v1 - Pas de sandbox UI. - Pas de hot-unload mémoire garanti dans la même session. - Pas de marketplace distant. - Pas de compilation TS/TSX. - Pas de `${projectRoot}` pour serveurs MCP plugin globaux. - Pas de garantie v1 sur `agentSelected`, `terminalFocused`, `layoutCellFocused` tant qu'un lot focus/selection n'a pas été cadré.