Ticket #43 carnet reflects activationScope in the plugin manifest example; ticket #140 closed; skills index/content refreshed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
11 KiB
issueRef, version, updatedBy, updatedAt
| issueRef | version | updatedBy | updatedAt | ||||
|---|---|---|---|---|---|---|---|
| #43 | 6 |
|
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 :
{app_data_dir}/plugins/
registry.json
installed/
<pluginId>/
idea-plugin.json
dist/index.js
assets/...
servers/...
États v1 persistés :
enabled
disabled
pending-enable
pending-disable
pending-uninstall
invalid
Règles :
- Installer depuis archive ou dossier local copie toujours un snapshot dans
installed/<pluginId>/. registry.jsonporte 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
enabledet nonpending-uninstall.
2. Manifeste v1
Fichier obligatoire : idea-plugin.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 :
trustLevelvaut uniquementfullen v1.activationScopeest optionnel, vautapppar défaut, ouprojectpour différer l'activation runtime jusqu'au premier projet focused ; l'étatpendingn'est pas une erreur de chargement.main,icon, assets et commandes MCP relatives ne doivent contenir ni chemin absolu ni...idstable, unique, reverse-DNS recommandé.versionSemVer.engines.ideaincompatible => plugininvalid, 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 :
export type LayoutNode =
| { type: "leaf"; node: LeafCell }
| { type: "split"; node: SplitContainer }
| { type: "grid"; node: GridContainer }
| { type: "customPluginLayout"; node: CustomPluginLayoutCell };
Payload canonique :
export interface CustomPluginLayoutCell {
id: string;
pluginId: string;
layoutType: string;
state: unknown;
}
Exemple complet :
{
"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 :
{
"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 :
{
"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 :
export type PluginLayoutAvailability =
| "available"
| "plugin-disabled"
| "plugin-missing"
| "incompatible";
Rendu :
available: rendre le composant React enregistré pourlayoutType.plugin-disabled,plugin-missing,incompatible: rendre le fallback non destructifLayout 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éetype: "customPluginLayout"est le contrat canonique. - À vérifier seulement : roundtrip serde et DTO Tauri exposent bien
pluginId,layoutType,stateen camelCase.
Frontend :
- Ajouter la variante
{ type: "customPluginLayout"; node: CustomPluginLayoutCell }àLayoutNode. - Retirer
pluginLayout?: CustomPluginLayoutCelldeLeafCellcomme contrat domaine. - Adapter
LayoutGrid/renderer récursif pour routertype === "customPluginLayout"versPluginLayoutCellView. - Adapter
layoutAvailability, fallback et tests pour recevoir le payload depuisnode.nodede 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 :
type MenuTargetId = "panels" | "settings" | `plugin:${string}`;
Items v1 :
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 :
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 deprojectOpen && gitRepository; le laisser àfalserend des contributions valides inatteignables.agentSelected,terminalFocused,layoutCellFocused: dette acceptable v1 si elles restent explicitement documentées comme best-effort non câblé et doncfalsejusqu'à 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 :
interface PluginMcpServerContribution {
id: string;
displayName: string;
command: string;
args?: string[];
env?: Record<string, string>;
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 :
- DevBackend implémente la substitution
${appDataDir}dans les specs MCP plugin et ajoute tests args/env/cwd. - Si le coût est refusé pour v1, retirer
${appDataDir}du contrat manifeste et du validator, puis documenter explicitement${pluginRoot}comme seule variable v1.
- DevBackend implémente la substitution
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
LayoutNodealignée sur Rust aveccustomPluginLayouttop-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
customPluginLayouttop-level rendu côté frontend. - Fallback atteint si plugin missing/disabled avec nœud top-level.
- Aucun fallback basé sur
leaf.node.pluginLayoutne 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,layoutCellFocusedtant qu'un lot focus/selection n'a pas été cadré.