---
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é.