Compare commits
3 Commits
31925dc1ce
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 9829ccdf2a | |||
| d1c3c00b4d | |||
| fe50219493 |
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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`);
|
||||
|
||||
Reference in New Issue
Block a user