Compare commits

..

3 Commits

Author SHA1 Message Date
9829ccdf2a docs(sdk): plugin minimal en dossier + hot reload documentés — manifest (arborescence minimale idea-plugin.json/package.json/tsconfig/src/dist, install depuis dossier), packaging-distribution (procédure hot reload directory install + idea_plugin_reload + limites UI React), README (lien packaging hot reload) 2026-09-08 09:39:25 +02:00
d1c3c00b4d feat(manifest): un plugin peut contribuer des commandes slash via callback — #165 (QA verte)
Étend le manifeste et la validation SDK : un plugin déclare des commandes
slash (contributes.slashCommands) adossées à une callback (command id
enregistré via ctx.commands.registerCommand). Métadonnées UI exposées :
name, shortDescription, requiresConfirmation, when. Exemple hello-plugin
mis à jour avec une commande /hello.

Ces commandes transitent ensuite par le registry/contrat unifié (#162).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 15:46:05 +02:00
fe50219493 merge(sdk): intègre feature/plugin-hosted-windows — runtime React partagé + docs restructurées (vert QA)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 00:40:05 +02:00
6 changed files with 190 additions and 3 deletions

View File

@ -15,7 +15,7 @@ exports `activate(ctx)`.
- [Layouts With React](docs/layouts-react.md): React/JSX/hooks layout authoring with host-provided React.
- [Windows](docs/windows.md): opening plugin layouts in detached OS windows.
- [Services](docs/services.md): workspace, tasks, tooling, events, config, terminal and windows facades.
- [Packaging And Distribution](docs/packaging-distribution.md): build output, archive layout and dependency rules.
- [Packaging And Distribution](docs/packaging-distribution.md): build output, archive layout, hot reload and dependency rules.
The installable example lives in [`examples/hello-plugin`](examples/hello-plugin).
It demonstrates a menu command, storage, background-task feedback, a React layout

View File

@ -5,14 +5,20 @@ background work.
## Contract
Every human menu click follows this sequence:
Every menu click or plugin slash command follows this sequence:
```text
menu item -> command id -> registered command handler -> optional task -> feedback surfaces
manifest contribution -> command id -> registered command handler -> optional task -> feedback surfaces
```
- A manifest menu item declares a `command` id; it does not run tools directly.
- A manifest slash command declares a slash `name`, autocomplete metadata and a
`command` id; it does not run tools directly.
- Menu items and slash commands may share the same `command` id, or point to
different handlers. The plugin owns that choice.
- The command handler is the only place that decides whether work should start.
- The host slash-command registry only lists/filters metadata and returns a
callback dispatch effect. The plugin handler decides what the command does.
- A launched process is represented by a background task returned from
`ctx.services.tasks.runCommand()`.
- A skipped command is represented by the command handler return value and logs,

View File

@ -2,6 +2,103 @@
Every plugin package has an `idea-plugin.json` file at the archive root.
For development installs, the same file must exist at the root of the plugin
source directory selected in IdeA with `Paramètres > Plugins > Installer depuis
un dossier…`.
## Minimal Plugin Directory
A plugin directory must contain the manifest at its root and a built ESM
entrypoint matching the manifest `main` field.
```text
my-plugin/
├── idea-plugin.json
├── package.json
├── tsconfig.json
├── src/
│ └── index.ts
└── dist/
└── index.js
```
Create the directory:
```sh
mkdir -p my-plugin/src
cd my-plugin
npm init -y
npm install --save-dev typescript @idea/plugin-sdk
```
Use a package script that emits JavaScript into `dist/`:
```json
{
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json"
},
"devDependencies": {
"@idea/plugin-sdk": "^0.3.0",
"typescript": "^5.0.0"
}
}
```
When developing against a local SDK checkout instead of a published package,
replace the SDK dependency with a `file:` reference, for example:
```json
{
"devDependencies": {
"@idea/plugin-sdk": "file:../IdeaSDK",
"typescript": "^5.0.0"
}
}
```
Minimal `tsconfig.json`:
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true
},
"include": ["src"]
}
```
Minimal `src/index.ts`:
```ts
import type { IdeAPluginModule } from "@idea/plugin-sdk";
const plugin: IdeAPluginModule = {
activate(ctx) {
ctx.logger.info("plugin activated", { pluginId: ctx.pluginId });
}
};
export default plugin;
```
Build before installing or reloading:
```sh
npm run build
```
Then install the `my-plugin/` directory in IdeA. For hot reload, keep installing
from the directory, rebuild after source changes, then run `idea_plugin_reload`
for the installed plugin id. Do not install from a ZIP archive for a hot-reload
development loop.
```json
{
"ideaPluginManifestVersion": 1,

View File

@ -74,3 +74,55 @@ npm run package:hello-plugin
The resulting archive has no wrapping parent directory and is ready for IdeA's
plugin installer.
## Hot Reload During Development
Install the plugin from a directory when you want IdeA to hot-reload changes
without restarting the app.
In IdeA, open `Paramètres > Plugins`, choose `Installer depuis un dossier…`,
and select the plugin source directory. That directory must contain
`idea-plugin.json` at its root, and the manifest `main` field must point to the
built entrypoint that exists inside the same directory, for example
`dist/index.js`.
```text
hello-plugin/
├── idea-plugin.json
├── package.json
└── dist/
├── index.js
└── core/
└── layout.js
```
After editing plugin source files, rebuild the plugin output first:
```sh
npm run build
```
Then ask an IdeA agent that has the plugin administration tool available to run
`idea_plugin_reload` with the installed plugin id:
```json
{
"pluginId": "com.example.hello-plugin"
}
```
The reload uses the recorded directory source from the plugin registry. It
re-reads and validates the manifest, recalculates the package hash, updates the
installed package, emits `plugin_reloaded`, and reconciles plugin MCP servers.
It does not accept an arbitrary path at reload time; install from the intended
development directory first.
Do not install from the ZIP archive for a development loop that needs hot
reload. Archive installs are fixed package snapshots. They are appropriate for
distribution, but the reload command is only defined for plugins installed from
a directory source.
Current limitation: backend registry state and plugin MCP/tool contributions are
reloaded without restarting IdeA. Frontend React contributions that are already
loaded in the current UI session may keep their existing module instance until
the relevant plugin surface is recreated or the app session is restarted.

View File

@ -32,6 +32,14 @@
"order": 10
}
],
"slashCommands": [
{
"name": "/hello",
"shortDescription": "Run the hello-plugin callback",
"command": "hello-plugin",
"requiresConfirmation": false
}
],
"layouts": [
{
"type": "hello-plugin.hello-world",

View File

@ -16,6 +16,7 @@ export interface IdeAPluginManifest {
contributes?: {
menus?: IdeAPluginTopLevelMenuContribution[];
menuItems?: IdeAPluginMenuItemContribution[];
slashCommands?: IdeAPluginSlashCommandContribution[];
layouts?: IdeAPluginLayoutContribution[];
mcpServers?: IdeAPluginMcpServerContribution[];
};
@ -47,6 +48,19 @@ export interface IdeAPluginMenuItemContribution {
when?: string;
}
export interface IdeAPluginSlashCommandContribution {
/** Slash name shown in autocomplete. Must start with "/". */
name: string;
/** Short autocomplete/help description. */
shortDescription: string;
/** Command callback id registered through ctx.commands.registerCommand(). */
command: string;
/** Ask for host confirmation before dispatching the callback. */
requiresConfirmation?: boolean;
/** Reserved declarative condition for host-side availability. */
when?: string;
}
export interface IdeAPluginLayoutContribution {
type: string;
label: string;
@ -196,6 +210,16 @@ function validateContributes(value: unknown, errors: string[]): void {
optionalString(item, "icon", errors, `contributes.menuItems[${index}].icon`);
optionalString(item, "when", errors, `contributes.menuItems[${index}].when`);
});
validateArray(value, "slashCommands", errors, (command, index) => {
requireString(command, "name", errors, `contributes.slashCommands[${index}].name`);
requireString(command, "shortDescription", errors, `contributes.slashCommands[${index}].shortDescription`);
requireString(command, "command", errors, `contributes.slashCommands[${index}].command`);
if (typeof command.name === "string" && !command.name.startsWith("/")) {
errors.push(`contributes.slashCommands[${index}].name must start with "/"`);
}
optionalBoolean(command, "requiresConfirmation", errors, `contributes.slashCommands[${index}].requiresConfirmation`);
optionalString(command, "when", errors, `contributes.slashCommands[${index}].when`);
});
validateArray(value, "layouts", errors, (layout, index) => {
requireString(layout, "type", errors, `contributes.layouts[${index}].type`);
requireString(layout, "label", errors, `contributes.layouts[${index}].label`);