Compare commits
2 Commits
9829ccdf2a
...
6eb8db3791
| Author | SHA1 | Date | |
|---|---|---|---|
| 6eb8db3791 | |||
| 1239104b0f |
@ -16,6 +16,7 @@ exports `activate(ctx)`.
|
|||||||
- [Windows](docs/windows.md): opening plugin layouts in detached OS windows.
|
- [Windows](docs/windows.md): opening plugin layouts in detached OS windows.
|
||||||
- [Services](docs/services.md): workspace, tasks, tooling, events, config, terminal and windows facades.
|
- [Services](docs/services.md): workspace, tasks, tooling, events, config, terminal and windows facades.
|
||||||
- [Packaging And Distribution](docs/packaging-distribution.md): build output, archive layout, hot reload and dependency rules.
|
- [Packaging And Distribution](docs/packaging-distribution.md): build output, archive layout, hot reload and dependency rules.
|
||||||
|
- [Codex Skills And IdeA Plugins](docs/codex-skills-and-idea-plugins.md): separate IdeA-native plugin skills from Codex plugin discovery.
|
||||||
|
|
||||||
The installable example lives in [`examples/hello-plugin`](examples/hello-plugin).
|
The installable example lives in [`examples/hello-plugin`](examples/hello-plugin).
|
||||||
It demonstrates a menu command, storage, background-task feedback, a React layout
|
It demonstrates a menu command, storage, background-task feedback, a React layout
|
||||||
|
|||||||
51
docs/codex-skills-and-idea-plugins.md
Normal file
51
docs/codex-skills-and-idea-plugins.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Codex Skills And IdeA Plugins
|
||||||
|
|
||||||
|
IdeA runtime plugins, IdeA-native plugin skills and Codex plugins are distinct
|
||||||
|
installation and discovery systems.
|
||||||
|
|
||||||
|
An IdeA runtime plugin is described by `idea-plugin.json`. Installing it through
|
||||||
|
IdeA makes its global runtime package available. Project settings then decide
|
||||||
|
which installed plugins are enabled for a project, and agent settings decide
|
||||||
|
which plugin skills and plugin MCP tools are assigned to each agent.
|
||||||
|
|
||||||
|
An IdeA-native plugin skill is declared in `idea-plugin.json` under
|
||||||
|
`contributes.skills`. Its body is a package-relative Markdown file such as
|
||||||
|
`skills/<name>/SKILL.md`. When the plugin is enabled for a project and the skill
|
||||||
|
is assigned to an agent, IdeA injects the skill affordance into that agent's
|
||||||
|
skill catalogue and serves the body through `idea_skill_read`. This is not
|
||||||
|
Codex-specific; it is projected by IdeA for every harness that supports IdeA
|
||||||
|
capabilities, including Claude Code, Codex, OpenCode local and OpenCode cloud.
|
||||||
|
|
||||||
|
A Codex plugin is described by `.codex-plugin/plugin.json`. Its `skills` entry
|
||||||
|
points Codex at skill directories such as `skills/*/SKILL.md`. This metadata is
|
||||||
|
for Codex plugin installation and skill discovery, not for IdeA runtime plugin
|
||||||
|
loading.
|
||||||
|
|
||||||
|
## Normative Behavior
|
||||||
|
|
||||||
|
- Installing or reloading an IdeA plugin makes the package available globally,
|
||||||
|
but does not automatically enable it in every project.
|
||||||
|
- Enabling a plugin for a project does not automatically assign every skill or
|
||||||
|
MCP tool to every agent.
|
||||||
|
- A plugin skill becomes visible to an agent only when the plugin is enabled for
|
||||||
|
the project and that skill is assigned to that agent.
|
||||||
|
- Installing or reloading an IdeA plugin does not automatically install a bundled
|
||||||
|
Codex plugin, and `.codex-plugin/plugin.json` remains inert for IdeA-native
|
||||||
|
plugin loading unless a separate bridge explicitly consumes it.
|
||||||
|
- Bundling `.codex-plugin/plugin.json` is still allowed as distribution content
|
||||||
|
for Codex-specific workflows, but it is not the multi-harness IdeA contract.
|
||||||
|
|
||||||
|
## Required Mechanisms
|
||||||
|
|
||||||
|
For multi-harness IdeA agent skills, declare `contributes.skills` in
|
||||||
|
`idea-plugin.json`, ship the referenced Markdown files inside the package,
|
||||||
|
install the plugin in IdeA, enable it in the target project, and assign the
|
||||||
|
skills to the target agents.
|
||||||
|
|
||||||
|
For Codex-specific plugin skills, install the Codex plugin through the Codex
|
||||||
|
plugin mechanism for the intended scope, then assign or enable those skills
|
||||||
|
according to Codex/IdeA agent configuration.
|
||||||
|
|
||||||
|
Do not rely on `.codex-plugin/plugin.json` as a cross-harness skill propagation
|
||||||
|
mechanism. Cross-harness plugin skill availability is driven by
|
||||||
|
`idea-plugin.json` and IdeA project/agent assignments.
|
||||||
@ -26,8 +26,9 @@ manifest contribution -> command id -> registered command handler -> optional ta
|
|||||||
- Feedback objects must be stable enough for agents and programmatic callers to
|
- Feedback objects must be stable enough for agents and programmatic callers to
|
||||||
parse, and their messages must be readable by humans.
|
parse, and their messages must be readable by humans.
|
||||||
- The current human menu-click UI does not guarantee display of a command
|
- The current human menu-click UI does not guarantee display of a command
|
||||||
handler return value. Use logs, Work/task state or plugin-owned UI/files when
|
handler return value. When a human must see feedback from a menu action, open
|
||||||
a human needs visible feedback today.
|
or focus a plugin layout/window from the handler with
|
||||||
|
`ctx.services.windows.open({ layoutType, state })`.
|
||||||
|
|
||||||
## Preconditions
|
## Preconditions
|
||||||
|
|
||||||
@ -76,6 +77,9 @@ Visible surfaces are intentionally distinct:
|
|||||||
- Command return value: immediate feedback for programmatic callers, agents and
|
- Command return value: immediate feedback for programmatic callers, agents and
|
||||||
future host surfaces. It is not a guaranteed visible UI surface for current
|
future host surfaces. It is not a guaranteed visible UI surface for current
|
||||||
human menu clicks.
|
human menu clicks.
|
||||||
|
- Plugin layout/window: the normative visible feedback surface for menu actions
|
||||||
|
that need to show status, results or next steps to a human. The layout must be
|
||||||
|
declared in `idea-plugin.json` and registered during activation.
|
||||||
- Plugin logs: diagnostics for developers and operators.
|
- Plugin logs: diagnostics for developers and operators.
|
||||||
- Work/background-task surfaces: only for tasks actually launched through
|
- Work/background-task surfaces: only for tasks actually launched through
|
||||||
`runCommand()`.
|
`runCommand()`.
|
||||||
@ -105,6 +109,40 @@ state when invoked instead of assuming a watch was installed at activation.
|
|||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
|
Visible menu feedback:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import type { IdeAPluginModule, PluginLayoutProps } from "@idea/plugin-sdk";
|
||||||
|
|
||||||
|
function HealthView(_props: PluginLayoutProps<{ source?: string }>) {
|
||||||
|
return <section>Unity tools are ready.</section>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin: IdeAPluginModule = {
|
||||||
|
activate(ctx) {
|
||||||
|
ctx.layouts?.register({
|
||||||
|
type: "unity-plugin.health",
|
||||||
|
component: HealthView
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.commands?.registerCommand("unity-plugin.health", async () => {
|
||||||
|
const win = await ctx.services?.windows.open({
|
||||||
|
layoutType: "unity-plugin.health",
|
||||||
|
state: { source: "menu" }
|
||||||
|
});
|
||||||
|
|
||||||
|
return win
|
||||||
|
? { status: "opened", message: "Opened Unity Health.", alreadyOpen: win.alreadyOpen }
|
||||||
|
: { status: "skipped", reason: "ui-service-unavailable", message: "UI service unavailable." };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default plugin;
|
||||||
|
```
|
||||||
|
|
||||||
|
Background task feedback:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const project = await ctx.services?.workspace.getCurrentProject();
|
const project = await ctx.services?.workspace.getCurrentProject();
|
||||||
if (!project) {
|
if (!project) {
|
||||||
|
|||||||
@ -32,6 +32,26 @@ ctx.layouts?.register({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `type` must exactly match one entry in `contributes.layouts`. The host
|
||||||
|
rejects registrations for undeclared layout types. A plugin that opens this
|
||||||
|
layout for human feedback must also declare the `ui` capability so the UI
|
||||||
|
runtime and window services are part of the public contract.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"capabilities": ["ui", "tooling"],
|
||||||
|
"contributes": {
|
||||||
|
"layouts": [
|
||||||
|
{
|
||||||
|
"type": "hello-plugin.dashboard",
|
||||||
|
"label": "Dashboard",
|
||||||
|
"component": "Dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
- `projectId`: project hosting the layout.
|
- `projectId`: project hosting the layout.
|
||||||
|
|||||||
@ -143,10 +143,99 @@ development loop.
|
|||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
|
`contributes.skills` declares read-only agent skills shipped inside the IdeA
|
||||||
|
plugin package. These skills are IdeA-native: IdeA can expose them in project
|
||||||
|
plugin settings, assign them to agents, inject them into the agent skill
|
||||||
|
catalogue, and serve their Markdown body through `idea_skill_read` for every
|
||||||
|
supported harness that receives IdeA capabilities, including Claude Code,
|
||||||
|
Codex, OpenCode local and OpenCode cloud.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"contributes": {
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"id": "unity.build-debug",
|
||||||
|
"name": "unity-build-debug",
|
||||||
|
"description": "Diagnose Unity build failures.",
|
||||||
|
"kind": "workflow",
|
||||||
|
"path": "skills/unity-build-debug/SKILL.md"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Skill contribution rules:
|
||||||
|
|
||||||
|
- `id` is stable within the plugin and is used by project/agent assignments.
|
||||||
|
- `name` is the agent-facing name shown in the injected skill catalogue and
|
||||||
|
accepted by `idea_skill_read`.
|
||||||
|
- `description` is the short affordance shown before the agent loads the skill.
|
||||||
|
- `kind` defaults to `"workflow"`; `"reference"` is also supported.
|
||||||
|
- `path` must be a package-relative Markdown file path ending in `.md`.
|
||||||
|
- Installing a plugin globally is not enough: a project must enable the plugin,
|
||||||
|
then assign the skill to the target agent.
|
||||||
|
|
||||||
`contributes.layouts` is the only surface for plugin layouts. The same layout
|
`contributes.layouts` is the only surface for plugin layouts. The same layout
|
||||||
contribution can be mounted inside an IdeA layout cell or opened in a detached OS
|
contribution can be mounted inside an IdeA layout cell or opened in a detached OS
|
||||||
window. Do not add a separate manifest contribution type for windows.
|
window. Do not add a separate manifest contribution type for windows.
|
||||||
|
|
||||||
|
For a command that opens a visible plugin window, declare both the menu command
|
||||||
|
and the layout type:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ideaPluginManifestVersion": 1,
|
||||||
|
"id": "com.example.unity-plugin",
|
||||||
|
"displayName": "Unity Developer Tools",
|
||||||
|
"publisher": "Example",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Unity integration for IdeA.",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"engines": {
|
||||||
|
"idea": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"trustLevel": "full",
|
||||||
|
"capabilities": ["ui", "tooling"],
|
||||||
|
"activationScope": "app",
|
||||||
|
"contributes": {
|
||||||
|
"menus": [
|
||||||
|
{
|
||||||
|
"id": "unity-plugin.menu",
|
||||||
|
"label": "Unity Developer Tools",
|
||||||
|
"topLevel": true,
|
||||||
|
"order": 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"menuItems": [
|
||||||
|
{
|
||||||
|
"id": "unity-plugin.health.item",
|
||||||
|
"targetMenuId": "unity-plugin.menu",
|
||||||
|
"label": "Health",
|
||||||
|
"command": "unity-plugin.health",
|
||||||
|
"order": 10,
|
||||||
|
"when": "projectOpen"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"layouts": [
|
||||||
|
{
|
||||||
|
"type": "unity-plugin.health",
|
||||||
|
"label": "Unity Health",
|
||||||
|
"component": "UnityHealth",
|
||||||
|
"order": 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`capabilities: ["ui"]` is required when a plugin relies on layouts or windows as
|
||||||
|
human-facing UI. Add `"tooling"` when the same plugin uses workspace, task,
|
||||||
|
tooling or terminal services. The `contributes.layouts[].type` value is the
|
||||||
|
public id used by both `ctx.layouts.register({ type, component })` and
|
||||||
|
`ctx.services.windows.open({ layoutType })`.
|
||||||
|
|
||||||
Contribution ids are part of the runtime contract:
|
Contribution ids are part of the runtime contract:
|
||||||
|
|
||||||
- commands can only register ids declared by `contributes.menuItems[*].command`;
|
- commands can only register ids declared by `contributes.menuItems[*].command`;
|
||||||
|
|||||||
@ -102,8 +102,23 @@ After editing plugin source files, rebuild the plugin output first:
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
Then ask an IdeA agent that has the plugin administration tool available to run
|
An IdeA agent can install a development plugin directory only when that agent has
|
||||||
`idea_plugin_reload` with the installed plugin id:
|
the plugin administration MCP tool explicitly allowed. The public tool is
|
||||||
|
`idea_plugin_install_from_directory`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"path": "relative/path/to/hello-plugin",
|
||||||
|
"expectedPluginId": "com.example.hello-plugin"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The path must resolve under the requester's project root. This installs the
|
||||||
|
directory as the recorded development source for later reloads.
|
||||||
|
|
||||||
|
After source edits, rebuild the plugin output and ask an IdeA agent that has the
|
||||||
|
plugin administration tool available to run `idea_plugin_reload` with the
|
||||||
|
installed plugin id:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -122,7 +137,17 @@ reload. Archive installs are fixed package snapshots. They are appropriate for
|
|||||||
distribution, but the reload command is only defined for plugins installed from
|
distribution, but the reload command is only defined for plugins installed from
|
||||||
a directory source.
|
a directory source.
|
||||||
|
|
||||||
Current limitation: backend registry state and plugin MCP/tool contributions are
|
On reload, `plugin_reloaded` tells the UI to rebuild the plugin runtime registry.
|
||||||
reloaded without restarting IdeA. Frontend React contributions that are already
|
When the package content hash changes, plugin ESM URLs change and React layout
|
||||||
loaded in the current UI session may keep their existing module instance until
|
components are loaded from the new package. Existing plugin layout instances may
|
||||||
the relevant plugin surface is recreated or the app session is restarted.
|
remount and lose local React state; host-persisted layout state written through
|
||||||
|
`setState` remains the durable state channel.
|
||||||
|
|
||||||
|
JavaScript module instances cannot be forcibly removed from the browser ESM
|
||||||
|
cache. Register handlers, layouts, watches and subscriptions through the SDK
|
||||||
|
registries and push disposable side effects into `ctx.subscriptions`; global
|
||||||
|
module side effects can otherwise survive a reload. If the package hash did not
|
||||||
|
change, the reload is idempotent and may reuse the same module instance.
|
||||||
|
|
||||||
|
IdeA runtime plugin installation is separate from Codex plugin/skill discovery.
|
||||||
|
See [Codex Skills And IdeA Plugins](codex-skills-and-idea-plugins.md).
|
||||||
|
|||||||
@ -10,9 +10,18 @@ await ctx.services?.windows.open({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`windows.open` is the public API for commands that need to make feedback visible
|
||||||
|
to a human. A command handler may still return structured data, but menu clicks
|
||||||
|
do not currently guarantee that return value is displayed.
|
||||||
|
|
||||||
## Contract
|
## Contract
|
||||||
|
|
||||||
- `layoutType` must match a layout type declared by the calling plugin.
|
- `layoutType` must match a layout type declared by the calling plugin.
|
||||||
|
- The plugin must declare `capabilities: ["ui"]` when it relies on plugin
|
||||||
|
layouts/windows as a human-facing surface. Add `"tooling"` only when the
|
||||||
|
plugin also uses tooling/workspace/task services.
|
||||||
|
- There is no `contributes.windows` manifest key. Windows always host an
|
||||||
|
existing `contributes.layouts` layout.
|
||||||
- The host validates the plugin is runtime-active and the layout exists before
|
- The host validates the plugin is runtime-active and the layout exists before
|
||||||
opening or focusing the window.
|
opening or focusing the window.
|
||||||
- Reopening the same plugin/layout pair focuses the existing window.
|
- Reopening the same plugin/layout pair focuses the existing window.
|
||||||
@ -38,9 +47,13 @@ The returned `label` is a host-owned window identity. Treat it as opaque.
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
ctx.commands?.registerCommand("hello-plugin.open-dashboard", async () => {
|
ctx.commands?.registerCommand("hello-plugin.open-dashboard", async () => {
|
||||||
return ctx.services?.windows.open({
|
const win = await ctx.services?.windows.open({
|
||||||
layoutType: "hello-plugin.dashboard",
|
layoutType: "hello-plugin.dashboard",
|
||||||
state: { source: "menu" }
|
state: { source: "menu" }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return win
|
||||||
|
? { status: "opened", message: "Opened dashboard.", alreadyOpen: win.alreadyOpen }
|
||||||
|
: { status: "skipped", reason: "ui-service-unavailable", message: "UI service unavailable." };
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|||||||
@ -6,6 +6,8 @@ export type {
|
|||||||
IdeAPluginLayoutContribution,
|
IdeAPluginLayoutContribution,
|
||||||
IdeAPluginMcpServerContribution,
|
IdeAPluginMcpServerContribution,
|
||||||
IdeAPluginMenuItemContribution,
|
IdeAPluginMenuItemContribution,
|
||||||
|
IdeAPluginSkillContribution,
|
||||||
|
IdeAPluginSkillKind,
|
||||||
IdeAPluginTopLevelMenuContribution
|
IdeAPluginTopLevelMenuContribution
|
||||||
} from "./manifest.js";
|
} from "./manifest.js";
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export interface IdeAPluginManifest {
|
|||||||
*/
|
*/
|
||||||
activationScope?: IdeAPluginActivationScope;
|
activationScope?: IdeAPluginActivationScope;
|
||||||
contributes?: {
|
contributes?: {
|
||||||
|
skills?: IdeAPluginSkillContribution[];
|
||||||
menus?: IdeAPluginTopLevelMenuContribution[];
|
menus?: IdeAPluginTopLevelMenuContribution[];
|
||||||
menuItems?: IdeAPluginMenuItemContribution[];
|
menuItems?: IdeAPluginMenuItemContribution[];
|
||||||
slashCommands?: IdeAPluginSlashCommandContribution[];
|
slashCommands?: IdeAPluginSlashCommandContribution[];
|
||||||
@ -26,10 +27,24 @@ export type IdeAPluginCapability = "ui" | "mcp" | "tooling";
|
|||||||
|
|
||||||
export type IdeAPluginActivationScope = "app" | "project";
|
export type IdeAPluginActivationScope = "app" | "project";
|
||||||
|
|
||||||
|
export type IdeAPluginSkillKind = "workflow" | "reference";
|
||||||
|
|
||||||
export interface IdeAPluginEngineConstraints {
|
export interface IdeAPluginEngineConstraints {
|
||||||
idea?: string;
|
idea?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IdeAPluginSkillContribution {
|
||||||
|
id: string;
|
||||||
|
/** Agent-facing name shown in the injected skill catalogue and accepted by idea_skill_read. */
|
||||||
|
name: string;
|
||||||
|
/** Short affordance shown to agents before they load the skill body. */
|
||||||
|
description: string;
|
||||||
|
/** Defaults to "workflow". */
|
||||||
|
kind?: IdeAPluginSkillKind;
|
||||||
|
/** Package-relative Markdown file, usually skills/<name>/SKILL.md. */
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IdeAPluginTopLevelMenuContribution {
|
export interface IdeAPluginTopLevelMenuContribution {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
@ -192,6 +207,20 @@ function validateContributes(value: unknown, errors: string[]): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateArray(value, "skills", errors, (skill, index) => {
|
||||||
|
requireString(skill, "id", errors, `contributes.skills[${index}].id`);
|
||||||
|
requireString(skill, "name", errors, `contributes.skills[${index}].name`);
|
||||||
|
requireString(skill, "description", errors, `contributes.skills[${index}].description`);
|
||||||
|
requireString(skill, "path", errors, `contributes.skills[${index}].path`);
|
||||||
|
if (typeof skill.kind === "string" && skill.kind !== "workflow" && skill.kind !== "reference") {
|
||||||
|
errors.push(`contributes.skills[${index}].kind must be "workflow" or "reference" when provided`);
|
||||||
|
} else if (skill.kind !== undefined && typeof skill.kind !== "string") {
|
||||||
|
errors.push(`contributes.skills[${index}].kind must be a string when provided`);
|
||||||
|
}
|
||||||
|
if (typeof skill.path === "string" && !skill.path.endsWith(".md")) {
|
||||||
|
errors.push(`contributes.skills[${index}].path must point to a .md file`);
|
||||||
|
}
|
||||||
|
});
|
||||||
validateArray(value, "menus", errors, (menu, index) => {
|
validateArray(value, "menus", errors, (menu, index) => {
|
||||||
requireString(menu, "id", errors, `contributes.menus[${index}].id`);
|
requireString(menu, "id", errors, `contributes.menus[${index}].id`);
|
||||||
requireString(menu, "label", errors, `contributes.menus[${index}].label`);
|
requireString(menu, "label", errors, `contributes.menus[${index}].label`);
|
||||||
|
|||||||
Reference in New Issue
Block a user