Compare commits

...

17 Commits

Author SHA1 Message Date
e3a0f46f9d merge(sdk): intègre feature/285-plugin-root-runtime-api — pluginRoot officiel activate(ctx) #285 (QA verte: npm run check) 2026-09-08 18:35:24 +02:00
9ce3a557a9 feat(sdk): pluginRoot officiel exposé dans activate(ctx) — #285 (src/runtime: champ pluginRoot = racine du package installé; docs activation-context/commands-and-feedback/services/manifest réécrits du constat d'absence vers le contrat d'usage officiel — runCommand reste project-scoped, pluginRoot utilisable dans command/args, substitution manifest ${pluginRoot}/${appDataDir} inchangée pour mcpServers; exemple hello-plugin: script packagé scripts/hello-task.mjs consommé via ctx.pluginRoot, packaging étendu; QA verte: npm run check — build, typecheck:examples, package:hello-plugin, artefact zip vérifié) 2026-09-08 18:35:24 +02:00
68019dec58 merge(sdk): intègre feature/285-plugin-root-scope-docs — clarification périmètre pluginRoot runtime #285 (QA verte: npm run build) 2026-09-08 17:20:05 +02:00
5f9c2f9193 docs(sdk): clarify pluginRoot scope for runtime plugins — #285 (activation-context: ActivateContext n'expose ni répertoire d'installation ni racine package ni file URL; commands-and-feedback + services: runCommand project/workspace-scoped, aucune résolution package-relative des scripts packagés; manifest: substitution ${pluginRoot}/${appDataDir} étendue par l'hôte uniquement dans contributes.mcpServers command/args/env/cwd; QA verte: npm run build, cohérence champs vs types SDK vérifiée) 2026-09-08 17:19:57 +02:00
7db819c647 merge(sdk): intègre feature/283-plugin-skill-assignment-docs — doc assignation skills plugin aux agents #283 (QA verte: npm run build, liens docs vérifiés) 2026-09-08 16:34:20 +02:00
1b5ab2fa86 docs(sdk): schéma project-plugins.json v1 + assignation skills/tools agents — #283 (project-plugin-assignments: workflow install→enable→assign→relaunch, schéma complet plugins/agents/skills/tools camelCase, ordre d'injection natif puis plugin, tombstones inertes uninstall/reinstall, validation UI/API idempotente vs édition manuelle, reload plugin ≠ relance agent; liens croisés manifest/codex-skills/README; QA verte: npm run build, liens docs vérifiés) 2026-09-08 16:34:03 +02:00
6eb8db3791 feat(sdk): contributes.skills — skills IdeA-native déclarables par plugin — #282 (types IdeAPluginSkillContribution/IdeAPluginSkillKind: id/name/description/kind workflow|reference/path .md package-relative, validation manifeste, export public; docs manifest + codex-skills-and-idea-plugins recentrés skills IdeA-native vs .codex-plugin Codex, README; QA verte: npm run build) 2026-09-08 15:48:47 +02:00
1239104b0f docs(sdk): feedback visible via windows.open + séparation Codex skills documentées — commands-and-feedback (surface normative + exemple), layouts-react (type↔contributes.layouts, capacité ui), manifest (exemple complet menu+layout+ui), windows (contrat ui, retour structuré), packaging-distribution (idea_plugin_install_from_directory, sémantique plugin_reloaded/ESM), nouveau codex-skills-and-idea-plugins, README (lien) — #280 QA verte 2026-09-08 13:40:52 +02:00
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
31925dc1ce docs(sdk): éclate et étend la documentation SDK par sujet
Remplace le README monolithique par un point d'entrée vers des pages dédiées
(manifest, activation/contexte, menus, commandes/feedback, layouts React,
fenêtres, services, packaging/distribution) pour couvrir #142-#144 et
faciliter la navigation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 00:39:30 +02:00
15f930dd3b feat(runtime): résout react/react-dom vers l'instance host pour les layouts plugin
Le runtime SDK expose désormais react/react-dom résolus contre l'instance
hébergée par IdeA plutôt qu'une copie embarquée, pour que les layouts plugin
en JSX/hooks partagent le même arbre React que l'host. hello-plugin migre son
layout d'exemple en .tsx pour illustrer le contrat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 00:39:25 +02:00
7e1bb5fd38 merge(sdk): intègre feature/sdk-package-doc — contrat command-and-feedback documenté (vert QA) 2026-08-03 15:51:38 +02:00
5b55996558 docs(sdk): fige le contrat command-and-feedback et aligne l'exemple hello-plugin
Documente la règle publique menu -> command handler -> tâche de fond optionnelle
-> feedback (docs/commands-and-feedback.md + README), clarifie via JSDoc les
invariants de runCommand/recordOnly/ownerAgentId dans runtime.ts, et met à jour
l'exemple hello-plugin (feedback structuré launched/skipped, watch non-fatal)
pour qu'il illustre fidèlement le contrat documenté. Publie docs/ dans le
package npm.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 15:51:30 +02:00
e509e796b4 feat(sdk): add plugin activationScope (app/project)
Manifests can now declare activationScope to defer activation until a
project is focused, instead of always activating at app bootstrap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 14:43:23 +02:00
6bca9cc4c0 feat(sdk): ESM multi-fichiers + storage plugin-owned (#134/#139)
Documente et illustre deux contrats SDK :

- Multi-fichiers ESM : le `main` du manifeste peut importer d'autres
  fichiers du package via specifiers relatifs, servis par IdeA sur
  `idea-plugin://` (build `tsc` non bundlé). Le packager embarque tout
  `dist/**/*.js` et vérifie la présence du `main`. Les bare specifiers
  (`node_modules`) restent hors contrat : à bundler ou vendorer.
- Storage plugin-owned : `ctx.storage` est la place canonique de l'état
  interne du plugin (compteurs, flags, préférences, caches), hors des
  fichiers projet. Les APIs workspace/config restent pour le contenu
  project-owned.

L'exemple hello-plugin est éclaté en modules (constants, core/layout,
core/workspace, core/storage) pour exercer l'import relatif et le storage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 12:28:30 +02:00
29 changed files with 1913 additions and 521 deletions

378
README.md
View File

@ -1,20 +1,27 @@
# IdeA Plugin SDK # IdeA Plugin SDK
Minimal public TypeScript SDK for IdeA plugins. Public TypeScript SDK for IdeA plugins.
This first version intentionally stays small: The SDK defines the stable manifest and runtime types used by plugins loaded by
IdeA. A plugin ships an `idea-plugin.json` manifest plus an ESM entrypoint that
exports `activate(ctx)`.
- public manifest types for `idea-plugin.json`; ## Start Here
- public runtime types for plugin modules exposing `activate(ctx)`;
- a stable `ctx.services` facade for workspace, background task and terminal operations; - [Manifest](docs/manifest.md): required fields, capabilities and contribution ids.
- public workspace file APIs for reading, writing, listing, stat and path resolution; - [Activation And Context](docs/activation-context.md): `activate(ctx)`, lifecycle, logging, storage and subscriptions.
- a bounded generic project-structure query API; - [Menus](docs/menus.md): top-level menus, native menu insertion and command registration.
- public command-task APIs for launching and tracking generic tools; - [Commands And Feedback](docs/commands-and-feedback.md): command return values, skipped work and background tasks.
- public external-toolchain diagnostics for executables, env vars and files; - [Layouts With React](docs/layouts-react.md): React/JSX/hooks layout authoring with host-provided React.
- public best-effort event subscriptions and workspace watch; - [Windows](docs/windows.md): opening plugin layouts in detached OS windows.
- public structured config-document helpers for JSON documents; - [Services](docs/services.md): workspace, tasks, tooling, events, config, terminal and windows facades.
- a lightweight manifest validator; - [Packaging And Distribution](docs/packaging-distribution.md): build output, archive layout, hot reload and dependency rules.
- a minimal `examples/hello-plugin` plugin. - [Codex Skills And IdeA Plugins](docs/codex-skills-and-idea-plugins.md): separate IdeA-native plugin skills from Codex plugin discovery.
- [Project Plugin Assignments](docs/project-plugin-assignments.md): enable plugins per project and assign skills and MCP tools to agents.
The installable example lives in [`examples/hello-plugin`](examples/hello-plugin).
It demonstrates a menu command, storage, background-task feedback, a React layout
component and `services.windows.open(...)`.
## Install ## Install
@ -28,356 +35,15 @@ npm install
npm run build npm run build
``` ```
## Typecheck the example ## Validate The Example
```sh ```sh
npm run typecheck:examples npm run typecheck:examples
```
## Build the installable hello plugin archive
```sh
npm run package:hello-plugin npm run package:hello-plugin
``` ```
The archive is written to: The example archive is written to:
```text ```text
examples/hello-plugin/build/hello-plugin-0.1.0.zip examples/hello-plugin/build/hello-plugin-0.1.0.zip
``` ```
Its ZIP root contains `idea-plugin.json` directly, with no wrapping parent directory. The
compiled ESM entrypoint is emitted at `dist/index.js`, matching the manifest `main` field.
## Plugin shape
An IdeA plugin ships an `idea-plugin.json` manifest and a JavaScript entrypoint built from
TypeScript.
```json
{
"ideaPluginManifestVersion": 1,
"id": "com.example.hello",
"displayName": "Hello Plugin",
"version": "0.1.0",
"main": "dist/index.js",
"trustLevel": "full",
"contributes": {}
}
```
The entrypoint exports an `activate(ctx)` function:
```ts
import type { ActivateContext } from "@idea/plugin-sdk";
export function activate(ctx: ActivateContext): void {
ctx.logger.info("hello from plugin");
}
```
## Layout Runtime
Plugins can contribute custom layout panels by declaring `contributes.layouts`
in `idea-plugin.json` and registering the matching layout type during
`activate(ctx)`.
```json
{
"contributes": {
"layouts": [
{
"type": "com.example.status",
"label": "Status",
"component": "StatusPanel"
}
]
}
}
```
```ts
import type { ActivateContext, PluginLayoutProps } from "@idea/plugin-sdk";
function StatusPanel(props: PluginLayoutProps): string {
return `status for ${props.projectId}`;
}
export function activate(ctx: ActivateContext): void {
const disposable = ctx.layouts?.register({
type: "com.example.status",
component: StatusPanel
});
if (disposable) ctx.subscriptions.push(disposable);
}
```
Public layout props are:
- `projectId`: project hosting the layout cell;
- `nodeId`: stable layout node id for that cell instance;
- `layoutType`: contributed layout type from the manifest;
- `state`: opaque JSON-serializable state persisted by the host;
- `setState(next)`: replaces that state;
- `availability`: currently `"available"` when the component is mounted.
Lifecycle: register layouts during `activate(ctx)`, keep the returned disposable
in `ctx.subscriptions`, and let the host dispose it on plugin unload. Layout
components may be mounted, unmounted and remounted by the host; keep durable UI
state in `state` via `setState`, not in module globals. Call `setState` from
user actions, effects or asynchronous callbacks, not unconditionally while
rendering. Services are available from `ctx.services` to plugins declaring the
`tooling` capability; layout props do not expose private runtime gateways.
## Runtime Services
Plugins declaring the `tooling` capability receive `ctx.services`. Plugins
without that capability do not receive this facade. Prefer `ctx.services` over
IdeA's internal runtime objects when it is available:
```ts
import type { ActivateContext } from "@idea/plugin-sdk";
export async function activate(ctx: ActivateContext): Promise<void> {
const project = await ctx.services?.workspace.getCurrentProject();
ctx.logger.info("current project", project);
const task = await ctx.services?.tasks.getStatus("task-id");
ctx.logger.info("task status", task?.status);
const terminal = await ctx.services?.terminal.open({ rows: 24, cols: 80 });
await terminal?.write(new TextEncoder().encode("echo hello\\r"));
}
```
### Workspace Files
Workspace paths are always relative to the project root. Hosts reject absolute
paths, `..`, empty path segments and paths outside the sandbox. Text APIs use
UTF-8; binary APIs use `Uint8Array`. Missing files reject on reads and resolve
to `{ exists: false }` from `stat`.
```ts
import type { ActivateContext } from "@idea/plugin-sdk";
export async function activate(ctx: ActivateContext): Promise<void> {
const workspace = ctx.services?.workspace;
const project = await workspace?.getCurrentProject();
if (!workspace || !project) return;
await workspace.writeTextFile(".ideai/hello-plugin.txt", "hello\n", project.id);
const file = await workspace.readTextFile(".ideai/hello-plugin.txt", project.id);
const listing = await workspace.listDirectory(".ideai", project.id);
const stat = await workspace.stat(file.path, project.id);
ctx.logger.info("workspace file", {
path: file.path,
bytes: stat.len,
entries: listing.entries.length
});
}
```
`watch(path, handler, projectId?)` subscribes to public workspace file-change
events for the given relative path. It is best-effort and bounded: plugins should
handle missed events by refreshing their own derived state when needed.
### Project Structure
`queryStructure()` returns a bounded, generic read model so plugins do not each
need to rescan the whole workspace for common markers:
```ts
const structure = await ctx.services?.workspace.queryStructure({
maxDepth: 3,
maxEntries: 500
});
for (const convention of structure?.conventions ?? []) {
console.log(convention.id, convention.markerPath);
}
```
The MVP detects generic marker-file conventions such as `package.json`,
`Cargo.toml`, `pyproject.toml`, `go.mod`, `Makefile` and `.git`. It deliberately
does not expose language-specific ASTs or Android-specific concepts.
Current terminal scope is intentionally minimal: it opens or reattaches a shell
PTY, writes bytes, resizes, detaches and closes.
### Command Tasks
Use `ctx.services.tasks.runCommand()` for non-interactive tools that should be
tracked as IdeA background tasks instead of opening a raw PTY. `command` and
`args` are passed separately, `cwd` is relative to the project root, and `env`
adds process environment variables. The current host requires an `ownerAgentId`
so the task can appear in Work and completion can be correlated to an agent.
```ts
import type { ActivateContext } from "@idea/plugin-sdk";
export async function activate(ctx: ActivateContext): Promise<void> {
const project = await ctx.services?.workspace.getCurrentProject();
if (!project) return;
const task = await ctx.services?.tasks.runCommand({
projectId: project.id,
ownerAgentId: "00000000-0000-0000-0000-000000000000",
label: "Check npm",
command: "npm",
args: ["--version"],
cwd: ".",
env: { CI: "1" },
recordOnly: true
});
const status = await ctx.services?.tasks.getCommandStatus(task.taskId);
ctx.logger.info("command task", {
taskId: task.taskId,
state: status?.state,
exitCode: status?.exitCode
});
}
```
`list`, `getStatus`, `attachOutput`, `cancel` and `retry` continue to operate on
tasks visible through IdeA's Work read model. `getCommandStatus` reads a launched
command task directly from the host task store.
### Toolchain Diagnostics
Use `ctx.services.tooling.diagnose()` to check external prerequisites without
hard-coding one stack into the SDK. A request can probe executables, inspect
environment variables and validate workspace files in one structured result.
```ts
import type { ActivateContext } from "@idea/plugin-sdk";
export async function activate(ctx: ActivateContext): Promise<void> {
const diagnostic = await ctx.services?.tooling.diagnose({
tools: [
{
id: "node",
executable: "node",
versionArgs: ["--version"],
required: true
}
],
env: [{ name: "PATH", required: true }],
files: [{ path: "package.json", kind: "file" }]
});
const node = diagnostic?.tools.find((tool) => tool.id === "node");
ctx.logger.info("tooling diagnostic", {
ok: diagnostic?.ok,
nodePresent: node?.present,
nodeVersion: node?.version,
messages: diagnostic?.messages
});
}
```
The diagnostic API is intentionally generic: it does not install tools, does not
model Android devices or emulators, and does not expose language-specific ASTs.
### Events And Watch
Use `ctx.services.events.subscribe()` for stable public host/project events. The
runtime hides the host polling details and returns a disposable subscription.
```ts
import type { ActivateContext } from "@idea/plugin-sdk";
export async function activate(ctx: ActivateContext): Promise<void> {
const subscription = await ctx.services?.events.subscribe(
{
eventTypes: ["backgroundTaskChanged"],
capacity: 100,
onDropped: (count) => ctx.logger.warn("plugin events dropped", { count })
},
(event) => {
if (event.type === "backgroundTaskChanged") {
ctx.logger.info("task changed", {
taskId: event.taskId,
state: event.state
});
}
}
);
if (subscription) ctx.subscriptions.push(subscription);
const watch = await ctx.services?.workspace.watch("src", (event) => {
ctx.logger.info("workspace changed", {
path: event.path,
kind: event.kind,
operation: event.operation
});
});
if (watch) ctx.subscriptions.push(watch);
}
```
Public event retention is `bestEffortBounded`: events are retained per
subscription up to the requested/host-capped capacity, drained oldest-first, and
`onDropped` reports when older retained events were overwritten.
### Structured Config Documents
Use `ctx.services.config` when a plugin needs to read or update a structured
configuration file without reimplementing parsing and serialization.
First-lot format support is deliberately narrow:
- `json` only;
- inferred from `.json` when `format` is omitted;
- serialized as pretty JSON with a trailing newline;
- update modes: `mergePatch` and `replace`;
- `mergePatch` follows JSON merge-patch semantics: object keys are merged
recursively and `null` removes a key.
```ts
import type { ActivateContext } from "@idea/plugin-sdk";
export async function activate(ctx: ActivateContext): Promise<void> {
const config = await ctx.services?.config.readDocument({
path: ".ideai/hello-plugin.json"
});
await ctx.services?.config.updateDocument({
path: ".ideai/hello-plugin.json",
mode: "mergePatch",
value: {
enabled: true,
lastReadFormat: config?.format ?? "json"
}
});
}
```
YAML, TOML, XML, `.properties` and stack-specific config models are not part of
this first lot.
Declare the additive `tooling` capability to receive `ctx.services` at runtime:
```json
{
"capabilities": ["ui", "tooling"]
}
```
## Manifest Validation
```ts
import { validatePluginManifest } from "@idea/plugin-sdk";
const result = validatePluginManifest(manifestJson);
if (!result.success) {
console.error(result.errors);
}
```
This validator is deliberately strict for core fields and permissive about future unknown fields.
It is not a security boundary.

View File

@ -0,0 +1,70 @@
# Activation And Context
The plugin entrypoint exports `activate(ctx)`.
```ts
import type { ActivateContext, IdeAPluginModule } from "@idea/plugin-sdk";
export function activate(ctx: ActivateContext): void {
ctx.logger.info("activated", {
pluginId: ctx.pluginId,
pluginRoot: ctx.pluginRoot
});
}
export default { activate } satisfies IdeAPluginModule;
```
IdeA accepts either a named `activate` export or a default export containing
`activate`.
## Activation Scope
`activationScope` defaults to `"app"`. App-scoped plugins activate during app
bootstrap. Project-scoped plugins are kept pending until a project is focused,
then activated once for the app session.
Choose `"project"` only when `activate(ctx)` must immediately read project
state. Menu commands and layouts can usually stay app-scoped and check for a
focused project at invocation/render time.
## Context Fields
- `pluginId`: host-provided plugin identity.
- `pluginRoot`: absolute host-local path to the active installed plugin package.
- `logger`: `debug`, `info`, `warn`, `error`.
- `subscriptions`: push disposables returned by command/layout/watch
registrations.
- `commands`: command registry for declared menu commands.
- `layouts`: layout registry for declared layout types.
- `menu`: marker for the plugin-owned menu surface.
- `storage`: plugin-owned persistent key/value storage.
- `services`: public host service facade for plugins declaring `ui` or `tooling`
capabilities.
The context never exposes internal IdeA gateways or Tauri commands. Use
`ctx.services` and the registration APIs instead.
`pluginRoot` identifies the committed package currently loaded by IdeA. It is
not the original source directory or archive path and may change after reinstall.
Treat it as read-only, do not persist it, and resolve packaged scripts/assets
under it only while the plugin is active. Workspace services remain confined to
project-owned paths.
## Disposal
Push every returned disposable to `ctx.subscriptions`:
```ts
const disposable = ctx.commands?.registerCommand("com.example.run", run);
if (disposable) ctx.subscriptions.push(disposable);
```
IdeA disposes these handles best-effort when the plugin is unloaded or the app
session ends.
## Storage
Use `ctx.storage` for plugin-owned counters, flags, preferences and small caches.
Use workspace/config services only for project-owned files or configuration that
the user expects to see in the project.

View File

@ -0,0 +1,53 @@
# 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. See
[Project Plugin Assignments](project-plugin-assignments.md) for the project
settings schema and the complete install, enable, assign, and relaunch workflow.
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.

View File

@ -0,0 +1,219 @@
# Commands And Feedback
This document is the normative SDK contract for plugin commands that may launch
background work.
## Contract
Every menu click or plugin slash command follows this sequence:
```text
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,
not by a fake task.
- Feedback objects must be stable enough for agents and programmatic callers to
parse, and their messages must be readable by humans.
- The current human menu-click UI does not guarantee display of a command
handler return value. When a human must see feedback from a menu action, open
or focus a plugin layout/window from the handler with
`ctx.services.windows.open({ layoutType, state })`.
## Preconditions
Check preconditions before calling `runCommand()`:
- `ctx.services` exists. Plugins need the `ui` or `tooling` capability for the
service facade.
- `ctx.services.workspace.getCurrentProject()` returned a project, or the caller
supplied a valid `projectId`.
- Required executables, environment variables and workspace files were validated,
preferably with `ctx.services.tooling.diagnose()`.
- `ownerAgentId` is a real agent id when the work is owned by an agent workflow.
Do not use all-zero placeholders in production.
- `cwd` is relative to the project root.
- `command` and `args` do not resolve package-relative plugin resources
implicitly. Build an explicit absolute script path from `ctx.pluginRoot` when
launching read-only files shipped in the installed package.
- Do not use `ctx.pluginRoot` as `cwd`; `cwd` remains relative to the project root.
- `command` and `args` are separate values. Do not shell-join user input.
If any required precondition fails, return a skipped result:
```ts
return {
status: "skipped",
reason: "missing-owner-agent",
message: "Configure an owner agent before launching the hello-plugin task."
};
```
## Feedback Surfaces
Command handlers should return one of these shapes, or a plugin-specific object
with equivalent fields:
```ts
type CommandFeedback =
| { status: "skipped"; reason: string; message: string }
| { status: "launched"; taskId: string; state: string; message: string };
```
Use the same `status` vocabulary consistently:
- `skipped`: no background task was created.
- `launched`: a task was created; inspect the task state for later progress.
- `failed`: the handler itself failed before it could return normally.
Visible surfaces are intentionally distinct:
- Command return value: immediate feedback for programmatic callers, agents and
future host surfaces. It is not a guaranteed visible UI surface for current
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.
- Work/background-task surfaces: only for tasks actually launched through
`runCommand()`.
- Plugin layouts or files: optional plugin-owned user feedback.
`recordOnly: true` affects completion delivery to the owning agent. It does not
mean hidden, skipped or UI-silent. A record-only command task can still appear in
Work and can still emit background-task events.
## Best-Effort Watches
`ctx.services.workspace.watch()` is not a baseline precondition for commands.
The host may reject it until workspace watch support is delivered. Treat watch
setup as optional and non-fatal:
```ts
try {
const watch = await ctx.services.workspace.watch(".ideai", refresh, project.id);
ctx.subscriptions.push(watch);
} catch (error) {
ctx.logger.info("workspace watch unavailable", { error });
}
```
Commands that depend on fresh workspace state should refresh or re-read that
state when invoked instead of assuming a watch was installed at activation.
## 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
const project = await ctx.services?.workspace.getCurrentProject();
if (!project) {
return {
status: "skipped",
reason: "no-focused-project",
message: "Open a project before running this command."
};
}
const ownerAgentId = await ctx.storage?.get<string>("myPlugin.ownerAgentId");
if (!ownerAgentId) {
return {
status: "skipped",
reason: "missing-owner-agent",
message: "Configure an owner agent before launching this task."
};
}
const task = await ctx.services.tasks.runCommand({
projectId: project.id,
ownerAgentId,
label: "Run my tool",
command: "npm",
args: ["--version"],
cwd: ".",
recordOnly: true
});
return {
status: "launched",
taskId: task.taskId,
state: task.state,
message: "Started Run my tool."
};
```
## Counterexample
Do not launch a shell command just to produce feedback:
```ts
await ctx.services?.tasks.runCommand({
ownerAgentId: "00000000-0000-0000-0000-000000000000",
label: "Skipped: missing config",
command: "echo",
args: ["missing config"],
recordOnly: true
});
```
This creates misleading Work history, uses a placeholder owner and turns a
precondition failure into a fake task. Return `status: "skipped"` instead.
Do not try to reach packaged plugin scripts through workspace-relative paths:
```ts
await ctx.services?.tasks.runCommand({
projectId: project.id,
ownerAgentId,
label: "Run packaged tests",
command: "scripts/run-unity-tests.sh",
cwd: "."
});
```
`scripts/run-unity-tests.sh` above is resolved like any other executable visible
from the project task environment; it is not resolved relative to the plugin
package. Use a project-owned script or an executable available on `PATH` until a
dedicated packaged-resource API exists.

105
docs/layouts-react.md Normal file
View File

@ -0,0 +1,105 @@
# Layouts With React
Plugin layouts are real React components. They can use JSX and hooks.
```tsx
import type { PluginLayoutProps } from "@idea/plugin-sdk";
import { useState } from "react";
export function Dashboard(props: PluginLayoutProps): React.ReactElement {
const [localClicks, setLocalClicks] = useState(0);
return (
<button
type="button"
onClick={() => {
setLocalClicks((value) => value + 1);
props.setState({ localClicks: localClicks + 1 });
}}
>
Clicked {localClicks}
</button>
);
}
```
Register the component for a manifest-declared layout type:
```ts
ctx.layouts?.register({
type: "hello-plugin.dashboard",
component: Dashboard
});
```
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
- `projectId`: project hosting the layout.
- `nodeId`: stable host node id for this layout instance.
- `layoutType`: manifest layout `type`.
- `state`: JSON-serializable host-persisted state.
- `setState(next)`: replace host-persisted state.
- `availability`: currently `"available"` while mounted.
Call `setState` from user actions, effects or async callbacks. Do not call it
unconditionally during render.
## Host React
Plugins must import React normally:
```ts
import { useEffect, useMemo, useState } from "react";
```
At runtime IdeA resolves these bare imports to the host instance:
- `react`
- `react-dom`
- `react-dom/client`
- `react/jsx-runtime`
- `react/jsx-dev-runtime`
Do not bundle your own copy of React or ReactDOM into the plugin. Declare them
as `peerDependencies` and `devDependencies` for local typechecking/builds.
```json
{
"peerDependencies": {
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
```
## Build
Use `jsx: "react-jsx"` in `tsconfig`. A plain multi-file ESM `tsc` build is
supported as long as every emitted file imported by `main` is included in the
archive.

271
docs/manifest.md Normal file
View File

@ -0,0 +1,271 @@
# Manifest
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,
"id": "com.example.hello-plugin",
"displayName": "Hello Plugin",
"publisher": "Example",
"version": "0.1.0",
"description": "Example IdeA plugin.",
"main": "dist/index.js",
"engines": {
"idea": ">=0.1.0"
},
"trustLevel": "full",
"capabilities": ["ui", "tooling"],
"activationScope": "app",
"contributes": {
"menus": [],
"menuItems": [],
"layouts": [],
"mcpServers": []
}
}
```
## Required Fields
- `ideaPluginManifestVersion`: currently `1`.
- `id`: stable lowercase id using letters, digits, dots and dashes. It must start
and end with an alphanumeric character.
- `displayName`: human-readable plugin name.
- `version`: semver version.
- `main`: package-relative ESM entrypoint loaded by IdeA.
- `trustLevel`: currently `"full"`.
## Optional Fields
- `description`, `publisher`.
- `engines.idea`: host compatibility hint.
- `activationScope`: `"app"` by default, or `"project"` when activation needs a
focused project immediately.
- `capabilities`: `"ui"`, `"tooling"` and/or `"mcp"`.
## 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.
See [Project Plugin Assignments](project-plugin-assignments.md) for the
`.ideai/project-plugins.json` schema, validation rules, assignment order, and
agent relaunch requirements.
`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
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:
- commands can only register ids declared by `contributes.menuItems[*].command`;
- layouts can only register `type` values declared by `contributes.layouts`;
- `services.windows.open({ layoutType })` only accepts a layout type declared by
the calling plugin.
## MCP Server Paths
`contributes.mcpServers` entries are resolved by the host before starting a
declared MCP server. In `command`, `args`, `env` and `cwd`, the host expands:
- `${pluginRoot}` to the installed plugin package root.
- `${appDataDir}` to the host-owned application data directory.
This string substitution is limited to manifest-declared MCP server startup.
Runtime handlers receive the same installed package location separately as
`ctx.pluginRoot`; `ctx.services.tasks.runCommand()` does not perform placeholder
substitution, and workspace APIs remain project-confined.
## Validation
Use the SDK validator in tests or build tooling:
```ts
import { assertPluginManifest } from "@idea/plugin-sdk";
assertPluginManifest(JSON.parse(manifestText));
```

56
docs/menus.md Normal file
View File

@ -0,0 +1,56 @@
# Menus
Menus are declared in the manifest and implemented by registering command
handlers during activation.
```json
{
"contributes": {
"menus": [
{
"id": "hello-plugin.menu",
"label": "Hello Plugin",
"topLevel": true,
"order": 100
}
],
"menuItems": [
{
"id": "hello-plugin.open.item",
"targetMenuId": "hello-plugin.menu",
"label": "Open Dashboard",
"command": "hello-plugin.open",
"order": 10,
"when": "projectOpen"
}
]
}
}
```
Then register the command id:
```ts
export function activate(ctx: ActivateContext): void {
const disposable = ctx.commands?.registerCommand("hello-plugin.open", async () => {
await ctx.services?.windows.open({ layoutType: "hello-plugin.dashboard" });
});
if (disposable) ctx.subscriptions.push(disposable);
}
```
## Rules
- A command can only be registered if at least one manifest menu item declares
that exact `command` id.
- Missing command handlers are a no-op when the user clicks the menu item.
- A handler owns precondition checks and feedback. See
[Commands And Feedback](commands-and-feedback.md).
- Menu item `when` expressions are evaluated by the host; unsupported or false
conditions hide/disable the item according to host policy.
## Targets
`targetMenuId` can refer to a plugin top-level menu id or a host menu id exposed
by IdeA. Prefer a plugin top-level menu for plugin-specific workflows and host
menus only when the action naturally belongs beside native actions.

View File

@ -0,0 +1,153 @@
# Packaging And Distribution
A plugin archive is a ZIP file whose root contains `idea-plugin.json`.
```text
hello-plugin-0.1.0.zip
├── idea-plugin.json
├── README.md
└── dist/
├── index.js
├── constants.js
└── core/
├── layout.js
├── storage.js
└── workspace.js
```
The manifest `main` field must point to an emitted file inside the archive:
```json
{
"main": "dist/index.js"
}
```
## Module Resolution
IdeA loads `main` as ESM and serves package-relative imports from the plugin
package. This is supported:
```js
import { Dashboard } from "./core/layout.js";
```
For React, import bare host modules normally:
```js
import { useState } from "react";
import { jsx } from "react/jsx-runtime";
```
IdeA resolves React/ReactDOM bare imports to the host instance. Other bare
dependencies are not host-resolved. Bundle or vendor third-party dependencies
other than React/ReactDOM into package-relative files.
## TypeScript Build
The hello plugin uses plain `tsc`:
```sh
npm run build
npm run build:hello-plugin
```
For React layouts, configure JSX:
```json
{
"compilerOptions": {
"jsx": "react-jsx",
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
```
## Archive Build
The SDK example can be packaged with:
```sh
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
```
An IdeA agent can install a development plugin directory only when that agent has
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
{
"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.
On reload, `plugin_reloaded` tells the UI to rebuild the plugin runtime registry.
When the package content hash changes, plugin ESM URLs change and React layout
components are loaded from the new package. Existing plugin layout instances may
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).

View File

@ -0,0 +1,154 @@
# Project Plugin Assignments
IdeA installs plugin packages globally, but enables their capabilities per
project and assigns agent-facing capabilities per agent. Project choices are
stored in the project-owned `.ideai/project-plugins.json` file.
The normal workflow is:
1. Install the plugin globally in IdeA.
2. Enable the installed plugin for the target project.
3. Assign selected plugin skills and MCP tools to each target agent.
4. Relaunch the agent so its capability snapshot includes the new assignments.
Installation alone does not enable a plugin in a project. Enabling a plugin does
not automatically grant all of its capabilities to every agent.
## Version 1 Schema
The current schema version is `1`. A complete example is:
```json
{
"version": 1,
"plugins": [
{
"pluginId": "com.example.unity-plugin",
"enabled": true
}
],
"agents": [
{
"agentId": "2a0e42e2-3c5f-4f30-a679-034b460cd59e",
"skills": [
{
"pluginId": "com.example.unity-plugin",
"skillId": "unity.build-debug"
}
],
"tools": [
{
"pluginId": "com.example.unity-plugin",
"serverId": "unity-editor",
"toolName": "unity_build"
}
]
}
]
}
```
The top-level fields are:
- `version`: required schema version. It must currently be `1`; unsupported
versions are rejected.
- `plugins`: project activation entries. Each entry contains the stable global
`pluginId` and an `enabled` boolean.
- `agents`: per-agent assignments. Each entry identifies an IdeA project agent
by `agentId` and contains ordered `skills` and `tools` arrays.
An agent skill reference contains:
- `pluginId`: the id from the owning plugin's `idea-plugin.json`.
- `skillId`: the stable `contributes.skills[].id` within that plugin.
An agent MCP tool reference contains:
- `pluginId`: the id from the owning plugin's `idea-plugin.json`.
- `serverId`: the stable `contributes.mcpServers[].id` within that plugin.
- `toolName`: the exact tool name advertised by that MCP server. The UI accepts
this name as text because a dynamic MCP tool catalogue is not yet available.
Missing `plugins`, `agents`, `skills`, or `tools` arrays default to empty arrays
when IdeA reads the document. Prefer writing them explicitly for clarity and
forward-compatible review.
## Assignment And Injection Order
Array order is significant. IdeA keeps plugin skill assignments in the order in
which they were added. At agent launch, IdeA resolves the agent's native skills
first, in agent-manifest order, then its plugin skills in the order stored in
`agents[].skills`. Unavailable assignments are skipped without reordering the
remaining skills.
The resolved skill catalogue is injected into the agent's orchestration context
before the agent persona. Each supported harness receives the same ordered
IdeA-native capability snapshot. The skill body remains read-only package
content and is available through `idea_skill_read` only when that skill was
resolved for the launched agent.
## Tombstones
Plugin activation entries and agent assignments are intentionally retained when
a plugin, skill, or MCP server is temporarily missing. These dangling entries
are tombstones: they preserve project intent across uninstall/reinstall or a
temporary manifest change.
A tombstone is inert. IdeA skips it when resolving capabilities, and the project
settings UI marks stale skill or server references as missing or obsolete. The
UI lets users remove those references explicitly. Reinstalling and activating a
matching plugin makes a retained assignment resolvable again.
Disabling a plugin also keeps its assignments. They become effective again only
after the plugin is enabled and runtime-active for the project.
## UI And API Validation
Prefer the project plugin settings UI or its backing API commands over manual
editing. They apply the following rules:
- Enabling requires the plugin package to be installed globally. Disabling a
missing plugin is allowed so a retained activation can be cleared.
- Assigning a skill requires an existing project agent, an enabled and
runtime-active plugin, and a matching `contributes.skills[].id`.
- Assigning a tool requires an existing project agent, an enabled and
runtime-active plugin, a matching `contributes.mcpServers[].id`, and a
non-empty `toolName`.
- Assign and unassign operations are idempotent. Assigning an existing reference
does not duplicate it; unassigning a missing reference is harmless.
- Unassign operations remain available for tombstones, so stale references can
be removed even when their plugin capability no longer resolves.
The project settings read API returns both the persisted document and the
currently installed plugin manifests. The UI uses this catalogue to offer only
skills and MCP servers from plugins enabled for the project, while still
surfacing persisted tombstones.
## Manual Editing
The file is project-owned and may be edited manually, for example in version
control or automation. Stop or relaunch affected agents around the edit, keep
the document valid JSON, preserve `version: 1`, use the real project agent UUID,
and copy plugin, skill, and server ids exactly from the plugin manifest.
Manual editing bypasses the mutation checks described above. A syntactically
valid document may therefore contain duplicates, unknown agents, disabled or
missing plugins, unknown capability ids, or an invalid tool name. Such entries
may remain inert or fail later operations. Use the UI/API whenever possible to
receive immediate validation and idempotent updates.
## Reloading Plugins And Relaunching Agents
These actions solve different problems:
- Reload the plugin after rebuilding or replacing its installed package. Reload
refreshes the plugin manifest, runtime bundle, contributions, and packaged
skill Markdown. It does not change project activation or agent assignments.
- Relaunch the agent after enabling or disabling a project plugin, changing its
skill/tool assignments, or manually editing `.ideai/project-plugins.json`.
Agent capabilities are resolved into a launch-time snapshot, so an already
running agent does not acquire assignment changes retroactively.
When both package content and assignments changed, reload the plugin first,
confirm it is enabled for the project, update the assignments, then relaunch the
agent.

87
docs/services.md Normal file
View File

@ -0,0 +1,87 @@
# Services
`ctx.services` is the stable public host facade. It is available to plugins that
declare `ui` or `tooling` capabilities.
```json
{
"capabilities": ["ui", "tooling"]
}
```
## Workspace
`services.workspace` reads the focused/current project, project context and
project-owned files. Paths are relative to the project root; hosts reject
absolute paths and traversal outside the workspace.
Key APIs:
- `getCurrentProject()`
- `getProjectRoot(projectId?)`
- `readProjectContext(projectId?)`
- `updateProjectContext(content, projectId?)`
- `readTextFile(path, projectId?)`
- `writeTextFile(path, content, projectId?)`
- `readBinaryFile(path, projectId?)`
- `writeBinaryFile(path, bytes, projectId?)`
- `listDirectory(path?, projectId?)`
- `stat(path, projectId?)`
- `watch(path, handler, projectId?)`
- `queryStructure(query?)`
## Tasks
`services.tasks` launches and inspects host-managed command tasks.
Use `runCommand()` only after preconditions are satisfied. `ownerAgentId` must be
a real agent id when work belongs to an agent workflow.
`runCommand()` keeps its `cwd` project/workspace scoped: `cwd` must be a relative
path under the project root. The host does not implicitly resolve `command` or
`args` against the plugin package. Use the absolute `ctx.pluginRoot` supplied at
activation time to construct an explicit path to a packaged script, and pass
that path as `command` or as an argument to its interpreter. Treat package files
as read-only.
```ts
const script = `${ctx.pluginRoot.replace(/[\\/]+$/, "")}/scripts/check.mjs`;
await ctx.services.tasks.runCommand({
projectId,
ownerAgentId,
label: "Run packaged check",
command: "node",
args: [script],
cwd: "."
});
```
## Tooling
`services.tooling.diagnose()` checks executables, environment values and files
from the host-controlled runtime.
## Events
`services.events.subscribe()` provides best-effort bounded subscriptions to
public plugin events such as workspace file changes and background task changes.
Dispose subscriptions when no longer needed.
## Config
`services.config` reads and updates structured JSON documents in project-owned
locations. Use it for configuration the user expects to review/version.
## Terminal
`services.terminal` opens, reattaches and closes terminal sessions:
- `open({ cwd?, rows?, cols?, onData? })`
- `reattach(sessionId, { onData? })`
- `close(sessionId)`
## Windows
`services.windows.open({ layoutType, state? })` opens a detached OS window for
one of the calling plugin's declared layout contributions. See
[Windows](windows.md).

59
docs/windows.md Normal file
View File

@ -0,0 +1,59 @@
# Windows
Plugin windows are detached OS windows that host an existing
`contributes.layouts` layout. There is no separate window contribution type.
```ts
await ctx.services?.windows.open({
layoutType: "hello-plugin.dashboard",
state: { openedFrom: "menu" }
});
```
`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
- `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
opening or focusing the window.
- Reopening the same plugin/layout pair focuses the existing window.
- `state` is JSON-serializable initial window state. The mounted component can
later call `setState(next)` to update its local host state.
- The window follows IdeA's focused project, matching detached native panel
behavior. If no project is focused, the host shows a neutral shell until one
is focused.
## Return Value
```ts
const win = await ctx.services.windows.open({ layoutType: "hello-plugin.dashboard" });
win.label;
win.alreadyOpen;
win.surface.layoutType;
```
The returned `label` is a host-owned window identity. Treat it as opaque.
## Typical Menu Flow
```ts
ctx.commands?.registerCommand("hello-plugin.open-dashboard", async () => {
const win = await ctx.services?.windows.open({
layoutType: "hello-plugin.dashboard",
state: { source: "menu" }
});
return win
? { status: "opened", message: "Opened dashboard.", alreadyOpen: win.alreadyOpen }
: { status: "skipped", reason: "ui-service-unavailable", message: "UI service unavailable." };
});
```

View File

@ -6,9 +6,32 @@ It exercises the current plugin primitives end to end:
- top-level menu: `Hello Plugin`; - top-level menu: `Hello Plugin`;
- menu entry: `hello-plugin`; - menu entry: `hello-plugin`;
- command: `hello-plugin`, returning `hello-world`; - command: `hello-plugin`, returning readable feedback:
- layout contribution: `hello-plugin.hello-world`, rendered as `hello-world`. `{ status: "launched", taskId, state, message }` when it starts a background
task, or `{ status: "skipped", reason, message }` when a precondition is not
met;
- layout contribution: `hello-plugin.hello-world`, rendered by a React/JSX
component with hooks.
- plugin window: the menu command opens the layout through
`ctx.services.windows.open(...)` when services are available.
- plugin-owned storage: activation count, command run count and initialization flag;
- tooling capability: logs the focused workspace project when `ctx.services` is available. - tooling capability: logs the focused workspace project when `ctx.services` is available.
- packaged command script: resolves `scripts/hello-task.mjs` from the official
`ctx.pluginRoot` and launches it with the project kept as the task working directory.
The source is intentionally split across multiple TypeScript modules:
- `src/index.ts` is the manifest entrypoint and imports relative ESM modules;
- `src/constants.ts` owns shared command/layout identifiers;
- `src/core/layout.tsx` and `src/core/workspace.ts` hold feature logic.
- `src/core/storage.ts` keeps plugin-owned counters and flags in `ctx.storage`.
- `scripts/hello-task.mjs` is a read-only runtime resource included in the archive.
The build uses plain `tsc`; it does not bundle the plugin into one file. The archive includes all
compiled `dist/**/*.js` files so IdeA can load `dist/index.js` and serve its package-relative imports
through `idea-plugin://`. React and ReactDOM are peer dependencies resolved to the host instance at
runtime. Other runtime imports from `node_modules` are outside this contract: vendor them as relative
files or bundle them into the plugin output before packaging.
```sh ```sh
npm run typecheck:examples npm run typecheck:examples
@ -16,18 +39,29 @@ npm run package:hello-plugin
``` ```
The installable archive is emitted at `examples/hello-plugin/build/hello-plugin-0.1.0.zip`. 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 It contains `idea-plugin.json` at the ZIP root, the compiled multi-file ESM output under `dist/`,
`dist/index.js`, matching the manifest `main` field. including `dist/index.js`, and the packaged handler under `scripts/hello-task.mjs`.
## Diagnostics ## Diagnostics
During activation the plugin logs: During activation the plugin logs:
- whether the command and layout runtime registries are available; - whether the command and layout runtime registries are available;
- whether plugin-owned storage is available;
- activation count and initialization state stored through `ctx.storage`;
- successful registration of the `hello-plugin` command; - successful registration of the `hello-plugin` command;
- successful registration of the `hello-plugin.hello-world` layout; - successful registration of the `hello-plugin.hello-world` layout;
- availability of the workspace service from the `tooling` runtime capability; - availability of the workspace service from the `tooling` runtime capability;
- the first layout render, including project/node identifiers. - best-effort workspace watch setup, including a non-fatal log when unavailable;
- the first layout render, including project/node identifiers;
- plugin window open/focus results from `ctx.services.windows.open(...)`.
During command invocation the plugin logs and returns structured feedback for
programmatic callers. The current human menu-click UI does not guarantee display
of that return value.
- skipped command feedback when no project or no `helloPlugin.ownerAgentId` is available;
- launched background command feedback when `helloPlugin.ownerAgentId` is configured;
These messages are intentionally small and stable so installation, bundle import, activation and These messages are intentionally small and stable so installation, bundle import, activation and
layout rendering failures can be separated quickly in IdeA logs/devtools. layout rendering failures can be separated quickly in IdeA logs/devtools.

View File

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

View File

@ -9,6 +9,16 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@idea/plugin-sdk": "file:../.." "@idea/plugin-sdk": "file:../.."
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"peerDependencies": {
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"
} }
}, },
"../..": { "../..": {
@ -16,12 +26,77 @@
"version": "0.1.0", "version": "0.1.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typescript": "^5.5.0" "typescript": "^5.5.0"
},
"peerDependencies": {
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"
} }
}, },
"node_modules/@idea/plugin-sdk": { "node_modules/@idea/plugin-sdk": {
"resolved": "../..", "resolved": "../..",
"link": true "link": true
},
"node_modules/@types/react": {
"version": "19.2.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/react": {
"version": "19.2.8",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.8",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.8"
}
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"dev": true,
"license": "MIT"
} }
} }
} }

View File

@ -9,6 +9,15 @@
}, },
"dependencies": { "dependencies": {
"@idea/plugin-sdk": "file:../.." "@idea/plugin-sdk": "file:../.."
},
"peerDependencies": {
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
} }
} }

View File

@ -0,0 +1 @@
console.log("hello from a script packaged with @idea/plugin-sdk");

View File

@ -0,0 +1,8 @@
export const COMMAND_ID = "hello-plugin";
export const LAYOUT_TYPE = "hello-plugin.hello-world";
export const STORAGE_KEYS = {
activationCount: "helloPlugin.activationCount",
commandRunCount: "helloPlugin.commandRunCount",
initialized: "helloPlugin.initialized",
ownerAgentId: "helloPlugin.ownerAgentId"
} as const;

View File

@ -0,0 +1,58 @@
import type { PluginLayoutProps } from "@idea/plugin-sdk";
import type { ReactElement } from "react";
import { useMemo, useState } from "react";
let hasLoggedFirstLayoutRender = false;
export function HelloWorldLayout(props: PluginLayoutProps): ReactElement {
const [localClicks, setLocalClicks] = useState(0);
const hostState = useMemo(() => {
return typeof props.state === "object" && props.state !== null && !Array.isArray(props.state)
? (props.state as Record<string, unknown>)
: {};
}, [props.state]);
const persistedClicks =
typeof hostState.clicks === "number" && Number.isFinite(hostState.clicks)
? hostState.clicks
: 0;
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 (
<section style={{ display: "grid", gap: 12, fontFamily: "system-ui, sans-serif" }}>
<header>
<h2 style={{ margin: 0, fontSize: 16 }}>Hello Plugin</h2>
<p style={{ margin: "4px 0 0", color: "#667085", fontSize: 13 }}>
React layout rendered by IdeA host React.
</p>
</header>
<dl style={{ display: "grid", gap: 4, margin: 0, fontSize: 13 }}>
<div>
<dt style={{ color: "#667085" }}>Project</dt>
<dd style={{ margin: 0 }}>{props.projectId}</dd>
</div>
<div>
<dt style={{ color: "#667085" }}>Layout</dt>
<dd style={{ margin: 0 }}>{props.layoutType}</dd>
</div>
</dl>
<button
type="button"
onClick={() => {
setLocalClicks((current) => current + 1);
props.setState({ ...hostState, clicks: persistedClicks + 1 });
}}
>
Persist click {persistedClicks} / local click {localClicks}
</button>
</section>
);
}

View File

@ -0,0 +1,32 @@
import type { ActivateContext } from "@idea/plugin-sdk";
import { STORAGE_KEYS } from "../constants.js";
export async function initializePluginStorage(ctx: ActivateContext): Promise<void> {
if (!ctx.storage) {
ctx.logger.warn("plugin-owned storage unavailable");
return;
}
const activationCount = await incrementStoredNumber(ctx, STORAGE_KEYS.activationCount);
const initialized = await ctx.storage.get<boolean>(STORAGE_KEYS.initialized);
if (!initialized) {
await ctx.storage.set(STORAGE_KEYS.initialized, true);
}
ctx.logger.info("plugin-owned storage ready", {
activationCount,
initialized: initialized ?? false
});
}
export async function recordCommandRun(ctx: ActivateContext): Promise<number | undefined> {
if (!ctx.storage) return undefined;
return incrementStoredNumber(ctx, STORAGE_KEYS.commandRunCount);
}
async function incrementStoredNumber(ctx: ActivateContext, key: string): Promise<number> {
const current = await ctx.storage?.get<number>(key);
const next = typeof current === "number" && Number.isFinite(current) ? current + 1 : 1;
await ctx.storage?.set(key, next);
return next;
}

View File

@ -0,0 +1,149 @@
import type { ActivateContext, CommandTaskStatus } from "@idea/plugin-sdk";
import { STORAGE_KEYS } from "../constants.js";
export type HelloCommandFeedback =
| {
status: "skipped";
reason: "services-unavailable" | "no-focused-project" | "missing-owner-agent";
message: string;
}
| {
status: "launched";
taskId: string;
state: CommandTaskStatus["state"];
message: string;
};
export async function useWorkspaceSdk(ctx: ActivateContext): Promise<void> {
const workspace = ctx.services?.workspace;
if (!workspace) return;
const project = await workspace.getCurrentProject();
if (!project) {
ctx.logger.info("workspace service available without a focused project");
return;
}
const listing = await workspace.listDirectory(".ideai", project.id);
const structure = await workspace.queryStructure({
projectId: project.id,
maxDepth: 2,
maxEntries: 100
});
ctx.logger.info("workspace project inspection complete", {
projectId: project.id,
ideaiEntries: listing.entries.length,
conventions: structure.conventions.map((convention) => convention.id)
});
const diagnostic = await ctx.services?.tooling.diagnose({
projectId: project.id,
tools: [
{
id: "echo",
executable: "echo",
versionArgs: ["hello-plugin-toolcheck"],
required: true
}
],
env: [{ name: "PATH", required: true }],
files: [{ path: "idea-plugin.json", kind: "file" }]
});
ctx.logger.info("tooling diagnostic complete", {
ok: diagnostic?.ok,
echoVersion: diagnostic?.tools.find((tool) => tool.id === "echo")?.version,
messages: diagnostic?.messages
});
try {
const watch = await workspace.watch(".ideai", (event) => {
ctx.logger.info("workspace watch event", {
path: event.path,
kind: event.kind,
operation: event.operation
});
}, project.id);
ctx.subscriptions.push(watch);
} catch (error) {
ctx.logger.info("workspace watch unavailable", { error });
}
const events = await ctx.services?.events.subscribe(
{
projectId: project.id,
eventTypes: ["backgroundTaskChanged"],
pollIntervalMs: 2000,
onDropped: (count) => ctx.logger.warn("plugin events dropped", { count })
},
(event) => {
if (event.type === "backgroundTaskChanged") {
ctx.logger.info("background task changed", {
taskId: event.taskId,
state: event.state
});
}
}
);
if (events) ctx.subscriptions.push(events);
}
export async function runHelloCommandTask(ctx: ActivateContext): Promise<HelloCommandFeedback> {
if (!ctx.services) {
const feedback = {
status: "skipped",
reason: "services-unavailable",
message: "The tooling service facade is unavailable for hello-plugin."
} as const;
ctx.logger.info("hello command skipped", feedback);
return feedback;
}
const project = await ctx.services.workspace.getCurrentProject();
if (!project) {
const feedback = {
status: "skipped",
reason: "no-focused-project",
message: "Open a project before launching the hello-plugin task."
} as const;
ctx.logger.info("hello command skipped", feedback);
return feedback;
}
const ownerAgentId = await ctx.storage?.get<string>(STORAGE_KEYS.ownerAgentId);
if (!ownerAgentId) {
const feedback = {
status: "skipped",
reason: "missing-owner-agent",
message: "Configure helloPlugin.ownerAgentId in plugin storage before launching the task."
} as const;
ctx.logger.info("hello command skipped", feedback);
return feedback;
}
const pluginRoot = ctx.pluginRoot.replace(/[\\/]+$/, "");
const task = await ctx.services.tasks.runCommand({
projectId: project.id,
ownerAgentId,
label: "Hello plugin command",
command: "node",
args: [`${pluginRoot}/scripts/hello-task.mjs`],
cwd: ".",
recordOnly: true
});
const status = await ctx.services.tasks.getCommandStatus(task.taskId);
const feedback = {
status: "launched",
taskId: task.taskId,
state: status?.state ?? task.state,
message: "Started the hello-plugin background command."
} as const;
ctx.logger.info("hello command task launched", {
...feedback,
exitCode: status?.exitCode ?? task.exitCode
});
return feedback;
}

View File

@ -1,34 +1,32 @@
import type { ActivateContext, IdeAPluginModule, PluginLayoutProps } from "@idea/plugin-sdk"; import type { ActivateContext, IdeAPluginModule } from "@idea/plugin-sdk";
import { COMMAND_ID, LAYOUT_TYPE } from "./constants.js";
const COMMAND_ID = "hello-plugin"; import { HelloWorldLayout } from "./core/layout.js";
const LAYOUT_TYPE = "hello-plugin.hello-world"; import { initializePluginStorage, recordCommandRun } from "./core/storage.js";
import { runHelloCommandTask, useWorkspaceSdk } from "./core/workspace.js";
let hasLoggedFirstLayoutRender = false;
function HelloWorldLayout(props: PluginLayoutProps): 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 { export function activate(ctx: ActivateContext): void {
ctx.logger.info("activating hello-plugin", { ctx.logger.info("activating hello-plugin", {
pluginId: ctx.pluginId, pluginId: ctx.pluginId,
hasCommands: Boolean(ctx.commands), hasCommands: Boolean(ctx.commands),
hasLayouts: Boolean(ctx.layouts) hasLayouts: Boolean(ctx.layouts),
hasStorage: Boolean(ctx.storage)
}); });
const commandDisposable = ctx.commands?.registerCommand(COMMAND_ID, () => { const commandDisposable = ctx.commands?.registerCommand(COMMAND_ID, async () => {
ctx.logger.info("command executed", { commandId: COMMAND_ID }); const commandRunCount = await recordCommandRun(ctx);
return "hello-world"; const feedback = await runHelloCommandTask(ctx);
const opened = await ctx.services?.windows.open({
layoutType: LAYOUT_TYPE,
state: { openedFromCommand: commandRunCount ?? null }
});
ctx.logger.info("command executed", { commandId: COMMAND_ID, commandRunCount, feedback });
if (opened) {
ctx.logger.info("layout window opened", {
label: opened.label,
alreadyOpen: opened.alreadyOpen
});
}
return feedback;
}); });
if (commandDisposable) { if (commandDisposable) {
@ -53,132 +51,10 @@ export function activate(ctx: ActivateContext): void {
ctx.logger.warn("layout registry unavailable", { layoutType: LAYOUT_TYPE }); ctx.logger.warn("layout registry unavailable", { layoutType: LAYOUT_TYPE });
} }
void initializePluginStorage(ctx);
void useWorkspaceSdk(ctx); void useWorkspaceSdk(ctx);
} }
async function useWorkspaceSdk(ctx: ActivateContext): Promise<void> {
const workspace = ctx.services?.workspace;
if (!workspace) return;
const project = await workspace.getCurrentProject();
if (!project) {
ctx.logger.info("workspace service available without a focused project");
return;
}
const fixturePath = ".ideai/hello-plugin.txt";
await workspace.writeTextFile(fixturePath, "hello from @idea/plugin-sdk\n", project.id);
const file = await workspace.readTextFile(fixturePath, project.id);
const stat = await workspace.stat(fixturePath, project.id);
const listing = await workspace.listDirectory(".ideai", project.id);
const structure = await workspace.queryStructure({
projectId: project.id,
maxDepth: 2,
maxEntries: 100
});
ctx.logger.info("workspace file round-trip complete", {
projectId: project.id,
path: file.path,
bytes: stat.len,
ideaiEntries: listing.entries.length,
conventions: structure.conventions.map((convention) => convention.id)
});
const diagnostic = await ctx.services?.tooling.diagnose({
projectId: project.id,
tools: [
{
id: "echo",
executable: "echo",
versionArgs: ["hello-plugin-toolcheck"],
required: true
}
],
env: [{ name: "PATH", required: true }],
files: [{ path: fixturePath, kind: "file" }]
});
ctx.logger.info("tooling diagnostic complete", {
ok: diagnostic?.ok,
echoVersion: diagnostic?.tools.find((tool) => tool.id === "echo")?.version,
messages: diagnostic?.messages
});
const configPath = ".ideai/hello-plugin.json";
await workspace.writeTextFile(
configPath,
JSON.stringify({ enabled: true, launches: 0 }, null, 2) + "\n",
project.id
);
const configDocument = await ctx.services?.config.readDocument({
projectId: project.id,
path: configPath
});
await ctx.services?.config.updateDocument({
projectId: project.id,
path: configPath,
mode: "mergePatch",
value: { lastFormat: configDocument?.format ?? "json", launches: 1 }
});
ctx.logger.info("config document updated", {
path: configDocument?.path,
format: configDocument?.format
});
const watch = await workspace.watch(".ideai", (event) => {
ctx.logger.info("workspace watch event", {
path: event.path,
kind: event.kind,
operation: event.operation
});
}, project.id);
ctx.subscriptions.push(watch);
const events = await ctx.services?.events.subscribe(
{
projectId: project.id,
eventTypes: ["backgroundTaskChanged"],
pollIntervalMs: 2000,
onDropped: (count) => ctx.logger.warn("plugin events dropped", { count })
},
(event) => {
if (event.type === "backgroundTaskChanged") {
ctx.logger.info("background task changed", {
taskId: event.taskId,
state: event.state
});
}
}
);
if (events) ctx.subscriptions.push(events);
const ownerAgentId = await ctx.storage?.get<string>("helloPlugin.ownerAgentId");
if (!ownerAgentId) {
ctx.logger.info("command task example skipped: no owner agent configured");
return;
}
const task = await ctx.services?.tasks.runCommand({
projectId: project.id,
ownerAgentId,
label: "Hello plugin command",
command: "echo",
args: ["hello from @idea/plugin-sdk"],
cwd: ".",
recordOnly: true
});
if (task) {
const status = await ctx.services?.tasks.getCommandStatus(task.taskId);
ctx.logger.info("command task launched", {
taskId: task.taskId,
state: status?.state ?? task.state,
exitCode: status?.exitCode ?? task.exitCode
});
}
}
const plugin: IdeAPluginModule = { const plugin: IdeAPluginModule = {
activate activate
}; };

View File

@ -14,6 +14,7 @@
} }
}, },
"include": [ "include": [
"src/**/*.ts" "src/**/*.ts",
"src/**/*.tsx"
] ]
} }

View File

@ -3,6 +3,7 @@
"compilerOptions": { "compilerOptions": {
"declaration": false, "declaration": false,
"declarationMap": false, "declarationMap": false,
"jsx": "react-jsx",
"noEmit": true, "noEmit": true,
"rootDir": "../..", "rootDir": "../..",
"paths": { "paths": {
@ -13,6 +14,7 @@
}, },
"include": [ "include": [
"src/**/*.ts", "src/**/*.ts",
"src/**/*.tsx",
"../../src/**/*.ts" "../../src/**/*.ts"
] ]
} }

65
package-lock.json generated
View File

@ -9,9 +9,74 @@
"version": "0.1.0", "version": "0.1.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typescript": "^5.5.0" "typescript": "^5.5.0"
},
"peerDependencies": {
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"
} }
}, },
"node_modules/@types/react": {
"version": "19.2.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/react": {
"version": "19.2.8",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.8",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.8"
}
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"dev": true,
"license": "MIT"
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",

View File

@ -1,7 +1,7 @@
{ {
"name": "@idea/plugin-sdk", "name": "@idea/plugin-sdk",
"version": "0.1.0", "version": "0.1.0",
"description": "Minimal public TypeScript SDK for IdeA plugins.", "description": "Public TypeScript SDK for IdeA plugins.",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@ -13,6 +13,7 @@
}, },
"files": [ "files": [
"dist", "dist",
"docs",
"README.md" "README.md"
], ],
"scripts": { "scripts": {
@ -28,7 +29,15 @@
"sdk" "sdk"
], ],
"license": "MIT", "license": "MIT",
"peerDependencies": {
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"
},
"devDependencies": { "devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typescript": "^5.5.0" "typescript": "^5.5.0"
} }
} }

View File

@ -1,4 +1,4 @@
import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
const pluginRoot = join(process.cwd(), "examples", "hello-plugin"); const pluginRoot = join(process.cwd(), "examples", "hello-plugin");
@ -8,9 +8,15 @@ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
const main = requireString(manifest, "main"); const main = requireString(manifest, "main");
const version = requireString(manifest, "version"); const version = requireString(manifest, "version");
const archivePath = join(pluginRoot, "build", `hello-plugin-${version}.zip`); const archivePath = join(pluginRoot, "build", `hello-plugin-${version}.zip`);
const distEntries = await collectFiles(join(pluginRoot, "dist"), "dist");
const scriptEntries = await collectFiles(join(pluginRoot, "scripts"), "scripts");
if (!distEntries.some((entry) => entry.archivePath === main)) {
throw new Error(`Built plugin dist does not contain manifest main: ${main}`);
}
const archiveEntries = [ const archiveEntries = [
{ archivePath: "idea-plugin.json", sourcePath: manifestPath }, { archivePath: "idea-plugin.json", sourcePath: manifestPath },
{ archivePath: main, sourcePath: join(pluginRoot, main) }, ...distEntries,
...scriptEntries,
{ archivePath: "README.md", sourcePath: join(pluginRoot, "README.md") } { archivePath: "README.md", sourcePath: join(pluginRoot, "README.md") }
]; ];
const DOS_TIME_MIDNIGHT = 0; const DOS_TIME_MIDNIGHT = 0;
@ -43,6 +49,23 @@ for (const entry of archiveEntries) {
await writeFile(archivePath, createZip(files)); await writeFile(archivePath, createZip(files));
console.log(`created ${archivePath}`); console.log(`created ${archivePath}`);
async function collectFiles(sourceDir, archiveDir) {
const entries = await readdir(sourceDir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const sourcePath = join(sourceDir, entry.name);
const archivePath = `${archiveDir}/${entry.name}`;
if (entry.isDirectory()) {
files.push(...await collectFiles(sourcePath, archivePath));
} else if (entry.isFile()) {
files.push({ archivePath, sourcePath });
}
}
return files.sort((left, right) => left.archivePath.localeCompare(right.archivePath));
}
function requireString(record, key) { function requireString(record, key) {
if (typeof record[key] !== "string" || record[key].trim().length === 0) { if (typeof record[key] !== "string" || record[key].trim().length === 0) {
throw new Error(`idea-plugin.json field "${key}" must be a non-empty string`); throw new Error(`idea-plugin.json field "${key}" must be a non-empty string`);

View File

@ -1,10 +1,13 @@
export type { export type {
IdeAPluginActivationScope,
IdeAPluginCapability, IdeAPluginCapability,
IdeAPluginManifest, IdeAPluginManifest,
IdeAPluginEngineConstraints, IdeAPluginEngineConstraints,
IdeAPluginLayoutContribution, IdeAPluginLayoutContribution,
IdeAPluginMcpServerContribution, IdeAPluginMcpServerContribution,
IdeAPluginMenuItemContribution, IdeAPluginMenuItemContribution,
IdeAPluginSkillContribution,
IdeAPluginSkillKind,
IdeAPluginTopLevelMenuContribution IdeAPluginTopLevelMenuContribution
} from "./manifest.js"; } from "./manifest.js";
export { export {
@ -42,6 +45,7 @@ export type {
IdeAPluginModule, IdeAPluginModule,
JsonValue, JsonValue,
LayoutRegistry, LayoutRegistry,
OpenPluginWindowOptions,
PluginLogger, PluginLogger,
PluginLayoutAvailability, PluginLayoutAvailability,
PluginLayoutComponent, PluginLayoutComponent,
@ -51,6 +55,7 @@ export type {
PluginLayoutState, PluginLayoutState,
PluginServices, PluginServices,
PluginStorage, PluginStorage,
PluginWindow,
ProjectConvention, ProjectConvention,
ProjectModule, ProjectModule,
ProjectStructure, ProjectStructure,
@ -81,5 +86,6 @@ export type {
WorkspaceTextFile, WorkspaceTextFile,
WorkspaceWatch, WorkspaceWatch,
WorkspaceWatchEvent, WorkspaceWatchEvent,
WorkspaceWatchHandler WorkspaceWatchHandler,
WindowService
} from "./runtime.js"; } from "./runtime.js";

View File

@ -9,9 +9,15 @@ export interface IdeAPluginManifest {
publisher?: string; publisher?: string;
engines?: IdeAPluginEngineConstraints; engines?: IdeAPluginEngineConstraints;
capabilities?: IdeAPluginCapability[]; capabilities?: IdeAPluginCapability[];
/**
* Defaults to "app". Use "project" when activate(ctx) requires a focused project.
*/
activationScope?: IdeAPluginActivationScope;
contributes?: { contributes?: {
skills?: IdeAPluginSkillContribution[];
menus?: IdeAPluginTopLevelMenuContribution[]; menus?: IdeAPluginTopLevelMenuContribution[];
menuItems?: IdeAPluginMenuItemContribution[]; menuItems?: IdeAPluginMenuItemContribution[];
slashCommands?: IdeAPluginSlashCommandContribution[];
layouts?: IdeAPluginLayoutContribution[]; layouts?: IdeAPluginLayoutContribution[];
mcpServers?: IdeAPluginMcpServerContribution[]; mcpServers?: IdeAPluginMcpServerContribution[];
}; };
@ -19,10 +25,26 @@ export interface IdeAPluginManifest {
export type IdeAPluginCapability = "ui" | "mcp" | "tooling"; export type IdeAPluginCapability = "ui" | "mcp" | "tooling";
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;
@ -41,6 +63,19 @@ export interface IdeAPluginMenuItemContribution {
when?: string; 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 { export interface IdeAPluginLayoutContribution {
type: string; type: string;
label: string; label: string;
@ -100,6 +135,7 @@ export function validatePluginManifest(input: unknown): PluginManifestValidation
validateEngines(input.engines, errors); validateEngines(input.engines, errors);
validateCapabilities(input.capabilities, errors); validateCapabilities(input.capabilities, errors);
validateActivationScope(input.activationScope, errors);
validateContributes(input.contributes, errors); validateContributes(input.contributes, errors);
if (errors.length > 0) { if (errors.length > 0) {
@ -151,6 +187,16 @@ function validateCapabilities(value: unknown, errors: string[]): void {
}); });
} }
function validateActivationScope(value: unknown, errors: string[]): void {
if (value === undefined) {
return;
}
if (value !== "app" && value !== "project") {
errors.push("activationScope must be \"app\" or \"project\" when provided");
}
}
function validateContributes(value: unknown, errors: string[]): void { function validateContributes(value: unknown, errors: string[]): void {
if (value === undefined) { if (value === undefined) {
return; return;
@ -161,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`);
@ -179,6 +239,16 @@ function validateContributes(value: unknown, errors: string[]): void {
optionalString(item, "icon", errors, `contributes.menuItems[${index}].icon`); optionalString(item, "icon", errors, `contributes.menuItems[${index}].icon`);
optionalString(item, "when", errors, `contributes.menuItems[${index}].when`); 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) => { validateArray(value, "layouts", errors, (layout, index) => {
requireString(layout, "type", errors, `contributes.layouts[${index}].type`); requireString(layout, "type", errors, `contributes.layouts[${index}].type`);
requireString(layout, "label", errors, `contributes.layouts[${index}].label`); requireString(layout, "label", errors, `contributes.layouts[${index}].label`);

View File

@ -1,5 +1,9 @@
import type { ComponentType, ReactNode } from "react";
export interface ActivateContext { export interface ActivateContext {
pluginId: string; pluginId: string;
/** Absolute host-local root of this active installed plugin package. */
pluginRoot: string;
logger: PluginLogger; logger: PluginLogger;
subscriptions: CommandDisposable[]; subscriptions: CommandDisposable[];
commands?: CommandRegistry; commands?: CommandRegistry;
@ -36,6 +40,10 @@ export interface CommandDisposable {
} }
export interface PluginStorage { export interface PluginStorage {
/**
* Plugin-owned persistent state. Use this for internal counters, flags,
* preferences and caches that should not be written into the user's project.
*/
get<T = unknown>(key: string): Promise<T | undefined>; get<T = unknown>(key: string): Promise<T | undefined>;
set<T = unknown>(key: string, value: T): Promise<void>; set<T = unknown>(key: string, value: T): Promise<void>;
delete(key: string): Promise<void>; delete(key: string): Promise<void>;
@ -43,7 +51,7 @@ export interface PluginStorage {
export type PluginLayoutState = JsonValue | undefined; export type PluginLayoutState = JsonValue | undefined;
export type PluginLayoutAvailability = "available"; export type PluginLayoutAvailability = "available";
export type PluginLayoutRenderResult = unknown; export type PluginLayoutRenderResult = ReactNode;
export interface PluginLayoutProps<TState extends PluginLayoutState = PluginLayoutState> { export interface PluginLayoutProps<TState extends PluginLayoutState = PluginLayoutState> {
/** Project currently hosting this layout cell. */ /** Project currently hosting this layout cell. */
@ -60,9 +68,8 @@ export interface PluginLayoutProps<TState extends PluginLayoutState = PluginLayo
availability: PluginLayoutAvailability; availability: PluginLayoutAvailability;
} }
export type PluginLayoutComponent<TState extends PluginLayoutState = PluginLayoutState> = ( export type PluginLayoutComponent<TState extends PluginLayoutState = PluginLayoutState> =
props: PluginLayoutProps<TState>, ComponentType<PluginLayoutProps<TState>>;
) => PluginLayoutRenderResult;
export interface PluginLayoutDefinition<TState extends PluginLayoutState = PluginLayoutState> { export interface PluginLayoutDefinition<TState extends PluginLayoutState = PluginLayoutState> {
/** Must match a layout `type` declared in this plugin's manifest. */ /** Must match a layout `type` declared in this plugin's manifest. */
@ -83,6 +90,7 @@ export interface PluginServices {
events: EventService; events: EventService;
config: ConfigDocumentService; config: ConfigDocumentService;
terminal: TerminalService; terminal: TerminalService;
windows: WindowService;
} }
export interface WorkspaceProject { export interface WorkspaceProject {
@ -124,6 +132,7 @@ export interface WorkspaceService {
/** /**
* Extension point for host file watching. The MVP SDK reserves the public * Extension point for host file watching. The MVP SDK reserves the public
* shape; hosts may reject with a clear not-implemented error until #127 lands. * shape; hosts may reject with a clear not-implemented error until #127 lands.
* Plugins must treat watch setup as best-effort and non-fatal.
*/ */
watch(path: string, handler: WorkspaceWatchHandler, projectId?: string): Promise<WorkspaceWatch>; watch(path: string, handler: WorkspaceWatchHandler, projectId?: string): Promise<WorkspaceWatch>;
/** Queries a bounded, generic project structure read model. */ /** Queries a bounded, generic project structure read model. */
@ -215,9 +224,19 @@ export interface ProjectStructure {
export interface BackgroundTaskStatus { export interface BackgroundTaskStatus {
taskId: string; taskId: string;
/**
* Agent that owns completion delivery and Work attribution for this task.
* This is host-assigned for existing tasks and should be treated as an opaque
* agent id by plugins.
*/
ownerAgentId: string; ownerAgentId: string;
projectId: string; projectId: string;
kind: string; kind: string;
/**
* Work read-model status. A skipped plugin command is not a background task
* and therefore never appears here; skipped commands should be reported by
* the command handler return value and plugin logs.
*/
status: "pending" | "running" | "completed" | "failed" | "cancelled" | "delivered"; status: "pending" | "running" | "completed" | "failed" | "cancelled" | "delivered";
exitCode: number | null; exitCode: number | null;
summary: string | null; summary: string | null;
@ -241,7 +260,11 @@ export interface BackgroundTaskRetryResult {
export interface RunCommandTaskOptions { export interface RunCommandTaskOptions {
/** Project that owns the command workspace. Defaults to the focused project. */ /** Project that owns the command workspace. Defaults to the focused project. */
projectId?: string; projectId?: string;
/** Agent id used by IdeA Work for ownership, cancellation and completion delivery. */ /**
* Real agent id used by IdeA Work for ownership, cancellation and completion
* delivery. Plugins must obtain this from host/plugin state for the workflow
* they are serving; placeholder ids are only acceptable in isolated examples.
*/
ownerAgentId: string; ownerAgentId: string;
/** Human-facing label shown in Work. Defaults to the command line. */ /** Human-facing label shown in Work. Defaults to the command line. */
label?: string; label?: string;
@ -253,7 +276,12 @@ export interface RunCommandTaskOptions {
cwd?: string; cwd?: string;
/** Extra environment variables for the command. */ /** Extra environment variables for the command. */
env?: Record<string, string> | Array<[string, string]>; env?: Record<string, string> | Array<[string, string]>;
/** When true, completion is recorded without waking the owner agent. */ /**
* When true, completion is recorded without waking the owner agent. This does
* not hide the task from Work/background-task surfaces and does not represent
* a skipped command. If preconditions fail, return readable command feedback
* instead of launching a record-only task.
*/
recordOnly?: boolean; recordOnly?: boolean;
/** Optional absolute deadline, epoch milliseconds. */ /** Optional absolute deadline, epoch milliseconds. */
deadlineMs?: number; deadlineMs?: number;
@ -261,9 +289,17 @@ export interface RunCommandTaskOptions {
export interface CommandTaskStatus { export interface CommandTaskStatus {
taskId: string; taskId: string;
/**
* Agent that owns this command task. The host uses it for correlation,
* cancellation and completion delivery.
*/
ownerAgentId: string; ownerAgentId: string;
projectId: string; projectId: string;
kind: string; kind: string;
/**
* Lifecycle state of a command task that was actually launched. There is no
* `skipped` state: skipped commands are command-handler feedback, not tasks.
*/
state: "queued" | "running" | "waiting" | "completed" | "failed" | "cancelled" | "expired"; state: "queued" | "running" | "waiting" | "completed" | "failed" | "cancelled" | "expired";
exitCode: number | null; exitCode: number | null;
summary: string | null; summary: string | null;
@ -475,7 +511,15 @@ export interface ConfigDocumentService {
} }
export interface BackgroundTaskService { export interface BackgroundTaskService {
/** Launches a non-interactive command as a first-class IdeA background task. */ /**
* Launches a non-interactive command as a first-class IdeA background task.
*
* Call this only after command preconditions are satisfied. The returned
* `CommandTaskStatus` means a task exists and can be inspected through command
* status APIs and Work/background-task surfaces. A plugin command that decides
* not to launch work should return readable command feedback, for example
* `{ status: "skipped", reason, message }`, and should not call `runCommand()`.
*/
runCommand(options: RunCommandTaskOptions): Promise<CommandTaskStatus>; runCommand(options: RunCommandTaskOptions): Promise<CommandTaskStatus>;
/** Reads one command task directly from the host task store. */ /** Reads one command task directly from the host task store. */
getCommandStatus(taskId: string): Promise<CommandTaskStatus | null>; getCommandStatus(taskId: string): Promise<CommandTaskStatus | null>;
@ -530,3 +574,32 @@ export interface TerminalService {
/** Kills a PTY by id. */ /** Kills a PTY by id. */
close(sessionId: string): Promise<void>; close(sessionId: string): Promise<void>;
} }
export interface OpenPluginWindowOptions {
/** Layout `type` declared by this plugin in `contributes.layouts`. */
layoutType: string;
/** Initial opaque state copied into the detached window surface. */
state?: JsonValue;
}
export interface PluginWindow {
label: string;
url: string;
alreadyOpen: boolean;
providerPluginDisplayName: string;
layoutLabel: string;
surface: {
pluginId: string;
layoutType: string;
state: JsonValue;
};
}
export interface WindowService {
/**
* Opens or focuses a detached IdeA OS window hosting one of this plugin's
* declared layout contributions. The host rejects layout ids absent from this
* plugin's manifest.
*/
open(options: OpenPluginWindowOptions): Promise<PluginWindow>;
}