Files
IdeA/.ideai/tickets/43/carnet.md
Blomios 98fb05447d chore(ideai): état runtime — tickets, mémoire, tâches de fond
État d'exécution accumulé (tickets créés/mis à jour hors #43, notes de
mémoire, tâche de fond) capturé au moment du commit de la feature.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 07:37:19 +02:00

11 KiB

issueRef, version, updatedBy, updatedAt
issueRef version updatedBy updatedAt
#43 5
kind agent_id
agent dce19c75-9669-4e45-b8de-9950025157da
1784667145901

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

{
  "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 :

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

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 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 :

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