feat(wave): #119/#122/#131/#132 verts + sprint plugins ESM/persistance #135/#136/#139

État d'intégration confiné à la branche batch. Les tickets #119 (skills →
capacités agent découvrables), #122 (override permissions par défaut), #131
(effort par agent/presets) et #132 (outil MCP d'édition du contexte projet)
sont verts en périmètre. Le sprint plugins multi-fichiers ESM / persistance
plugin-owned (#135/#136/#139) est co-implémenté dans les MÊMES fichiers de
câblage (frontend/src/ports/index.ts, backend/src/lib.rs, domain/ports.rs,
backend/dto.rs), inséparable sans staging interactif (indisponible ici).

Commit unique volontaire : préserve l'état vert QA sans découpe hunk risquée.
NON mergé vers develop tant que #137 (QA e2e plugins) n'est pas vert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 11:06:23 +02:00
parent 22c6bd803d
commit 171c6c923c
59 changed files with 3654 additions and 291 deletions

View File

@ -15,7 +15,7 @@
* collected, never thrown past `loadPlugins`.
*/
import type { PluginContributionDto, PluginRuntimePlugin } from "@/domain";
import type { JsonValue, PluginContributionDto, PluginRuntimePlugin } from "@/domain";
import {
PluginCommandRegistry,
PluginLayoutRegistry,
@ -38,9 +38,16 @@ export interface IdeaPluginContext {
commands: PluginCommandContext;
layouts: PluginLayoutRegistry;
menu: PluginMenuRegistry;
storage: PluginStorage;
services?: PluginServices;
}
export interface PluginStorage {
get<T extends JsonValue = JsonValue>(key: string): Promise<T | undefined>;
set(key: string, value: JsonValue): Promise<void>;
delete(key: string): Promise<void>;
}
export interface PluginActivation {
dispose?: () => void | Promise<void>;
}
@ -241,6 +248,7 @@ async function loadOne(
const commands = new PluginCommandRegistry(pluginId, declaredCommandIds);
const layouts = new PluginLayoutRegistry(pluginId, declaredLayoutTypes);
const menu = new PluginMenuRegistry(pluginId);
const storage = createPluginStorage(gateways, pluginId);
const ctx: IdeaPluginContext = {
pluginId,
@ -251,6 +259,7 @@ async function loadOne(
commands: createCommandContext(commands),
layouts,
menu,
storage,
};
if (hasCapability(entry, "tooling")) {
ctx.services = createPluginServices(gateways);
@ -287,6 +296,21 @@ async function loadOne(
}
}
function createPluginStorage(gateways: PluginGatewaySet, pluginId: string): PluginStorage {
return {
async get<T extends JsonValue = JsonValue>(key: string): Promise<T | undefined> {
const value = await gateways.pluginStorage.get({ pluginId, key });
return value === null ? undefined : (value as T);
},
async set(key, value) {
await gateways.pluginStorage.set({ pluginId, key, value });
},
async delete(key) {
await gateways.pluginStorage.delete({ pluginId, key });
},
};
}
/**
* Loads every plugin in the catalog into a fresh {@link PluginRuntimeRegistry}.
* Called once at app bootstrap (and never again in the same session — carnet