Ticket #92 rouvert, mises à jour de carnets/tâches de fond. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
349 lines
11 KiB
Markdown
349 lines
11 KiB
Markdown
---
|
|
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/
|
|
<pluginId>/
|
|
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/<pluginId>/`.
|
|
- `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",
|
|
"capabilities": ["ui", "mcp"],
|
|
"contributes": {
|
|
"menus": [],
|
|
"menuItems": [],
|
|
"layouts": [],
|
|
"mcpServers": []
|
|
},
|
|
"archive": {
|
|
"files": ["idea-plugin.json", "dist/**", "assets/**", "servers/**"]
|
|
}
|
|
}
|
|
```
|
|
|
|
Validation :
|
|
|
|
- `trustLevel` vaut uniquement `full` en v1.
|
|
- `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<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 :
|
|
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é. |