fix(plugins): isole les menus plugin et durcit hello-plugin SDK contre la rechute #120

Requalification Architect du 2026-08-01: le crash INTERFACE INTERROMPUE au
chargement d'un plugin (menu + item) remonte via ProjectsView -> usePluginMenus
-> MenuBar jusqu'à RootErrorBoundary, quel que soit le plugin (ancien ou
reconstruit via SDK).

- usePluginMenus isole la résolution/conversion des contributions plugin :
  toute erreur retombe sur [] au lieu de propager.
- ProjectsView sépare les menus natifs des menus enrichis par plugin et rend
  MenuBar derrière une error boundary locale (fallback menus natifs seuls).
- hello-plugin (SDK) et les tests d'installation associés durcis en cohérence.

Bookkeeping ticket #120 uniquement (issue.md, carnet.md) ; les fichiers
counter.json/index.json et le dossier tickets/122/ restent hors commit car
une collision de numérotation #122 existe entre cette base et
feature/ticket120-hello-plugin-install-path-audit (deux tickets différents
revendiquent #122) — à arbitrer avant de committer le bookkeeping global.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 16:23:56 +02:00
parent 8031d86deb
commit 5a30ec8b9c
11 changed files with 516 additions and 75 deletions

View File

@ -1,6 +1,13 @@
# Hello Plugin
Minimal IdeA plugin example using the public SDK types.
Installable IdeA plugin example rebuilt from the public SDK types.
It exercises the current plugin primitives end to end:
- top-level menu: `Hello Plugin`;
- menu entry: `hello-plugin`;
- command: `hello-plugin`, returning `hello-world`;
- layout contribution: `hello-plugin.hello-world`, rendered as `hello-world`.
```sh
npm run typecheck:examples
@ -10,3 +17,15 @@ npm run package:hello-plugin
The installable archive is emitted at `examples/hello-plugin/build/hello-plugin-0.1.0.zip`.
It contains `idea-plugin.json` at the ZIP root and the compiled ESM entrypoint at
`dist/index.js`, matching the manifest `main` field.
## Diagnostics
During activation the plugin logs:
- whether the command and layout runtime registries are available;
- successful registration of the `hello-plugin` command;
- successful registration of the `hello-plugin.hello-world` layout;
- the first layout render, including project/node identifiers.
These messages are intentionally small and stable so installation, bundle import, activation and
layout rendering failures can be separated quickly in IdeA logs/devtools.

View File

@ -4,7 +4,7 @@
"displayName": "Hello Plugin",
"publisher": "IdeA Examples",
"version": "0.1.0",
"description": "Minimal IdeA plugin example.",
"description": "SDK example plugin for validating command, menu and layout loading.",
"main": "dist/index.js",
"engines": {
"idea": ">=0.1.0"
@ -17,16 +17,26 @@
"menus": [
{
"id": "hello-plugin.menu",
"label": "Hello",
"topLevel": true
"label": "Hello Plugin",
"topLevel": true,
"order": 100
}
],
"menuItems": [
{
"id": "hello-plugin.sayHello.item",
"id": "hello-plugin.command.item",
"targetMenuId": "hello-plugin.menu",
"label": "Say Hello",
"command": "hello-plugin.sayHello"
"label": "hello-plugin",
"command": "hello-plugin",
"order": 10
}
],
"layouts": [
{
"type": "hello-plugin.hello-world",
"label": "hello-world",
"component": "hello-world",
"order": 10
}
]
}

View File

@ -1,15 +1,75 @@
import type { ActivateContext, IdeAPluginModule } from "@idea/plugin-sdk";
import type { ActivateContext, CommandDisposable, IdeAPluginModule } from "@idea/plugin-sdk";
const COMMAND_ID = "hello-plugin";
const LAYOUT_TYPE = "hello-plugin.hello-world";
type HelloPluginLayoutProps = {
projectId?: string;
nodeId?: string;
layoutType?: string;
state?: unknown;
};
type LayoutRegistry = {
register(definition: {
type: string;
component: (props: HelloPluginLayoutProps) => string;
}): CommandDisposable;
};
type HelloPluginContext = ActivateContext & {
layouts?: LayoutRegistry;
};
let hasLoggedFirstLayoutRender = false;
function HelloWorldLayout(props: HelloPluginLayoutProps): string {
if (!hasLoggedFirstLayoutRender) {
hasLoggedFirstLayoutRender = true;
console.info("[hello-plugin] layout first render", {
projectId: props.projectId,
nodeId: props.nodeId,
layoutType: props.layoutType,
hasState: props.state !== undefined
});
}
return "hello-world";
}
export function activate(ctx: ActivateContext): void {
ctx.logger.info("Hello from the IdeA hello plugin.");
const disposable = ctx.commands?.registerCommand("hello-plugin.sayHello", () => {
ctx.logger.info("Hello command executed.");
return "Hello from IdeA";
const pluginContext = ctx as HelloPluginContext;
ctx.logger.info("activating hello-plugin", {
pluginId: ctx.pluginId,
hasCommands: Boolean(pluginContext.commands),
hasLayouts: Boolean(pluginContext.layouts)
});
if (disposable) {
ctx.subscriptions.push(disposable);
const commandDisposable = pluginContext.commands?.registerCommand(COMMAND_ID, () => {
ctx.logger.info("command executed", { commandId: COMMAND_ID });
return "hello-world";
});
if (commandDisposable) {
ctx.subscriptions.push(commandDisposable);
ctx.logger.info("command registered", { commandId: COMMAND_ID });
} else {
ctx.logger.warn("command registry unavailable", { commandId: COMMAND_ID });
}
const layoutDisposable = pluginContext.layouts?.register({
type: LAYOUT_TYPE,
component: HelloWorldLayout
});
if (layoutDisposable) {
ctx.subscriptions.push(layoutDisposable);
ctx.logger.info("layout registered", {
layoutType: LAYOUT_TYPE,
component: "hello-world"
});
} else {
ctx.logger.warn("layout registry unavailable", { layoutType: LAYOUT_TYPE });
}
}
@ -18,4 +78,3 @@ const plugin: IdeAPluginModule = {
};
export default plugin;