Compare commits

..

5 Commits

Author SHA1 Message Date
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
22 changed files with 1039 additions and 471 deletions

434
README.md
View File

@ -1,21 +1,25 @@
# 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)`;
- plugin-owned persistent storage through `ctx.storage`; - [Manifest](docs/manifest.md): required fields, capabilities and contribution ids.
- a stable `ctx.services` facade for workspace, background task and terminal operations; - [Activation And Context](docs/activation-context.md): `activate(ctx)`, lifecycle, logging, storage and subscriptions.
- public workspace file APIs for reading, writing, listing, stat and path resolution; - [Menus](docs/menus.md): top-level menus, native menu insertion and command registration.
- a bounded generic project-structure query API; - [Commands And Feedback](docs/commands-and-feedback.md): command return values, skipped work and background tasks.
- public command-task APIs for launching and tracking generic tools; - [Layouts With React](docs/layouts-react.md): React/JSX/hooks layout authoring with host-provided React.
- public external-toolchain diagnostics for executables, env vars and files; - [Windows](docs/windows.md): opening plugin layouts in detached OS windows.
- public best-effort event subscriptions and workspace watch; - [Services](docs/services.md): workspace, tasks, tooling, events, config, terminal and windows facades.
- public structured config-document helpers for JSON documents; - [Packaging And Distribution](docs/packaging-distribution.md): build output, archive layout and dependency rules.
- a lightweight manifest validator;
- a minimal `examples/hello-plugin` plugin. 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
@ -29,413 +33,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,
alongside the other compiled files imported by that entrypoint.
## 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",
"activationScope": "app",
"contributes": {}
}
```
`activationScope` is optional and defaults to `"app"`, which activates the
plugin at app bootstrap without requiring a focused project. Use
`"activationScope": "project"` only when `activate(ctx)` needs project-scoped
services immediately; IdeA will keep that plugin pending until a project is
focused, then activate it once for the app session.
The `main` field is the ESM entrypoint loaded by IdeA. It may import other
JavaScript files from the same plugin package with relative specifiers:
```js
import { COMMAND_ID } from "./constants.js";
import { useWorkspaceSdk } from "./core/workspace.js";
```
Those relative imports are served by IdeA through the `idea-plugin://` protocol,
so plugins do not need to be bundled into a single JavaScript file. A plain
`tsc` build that emits multiple ESM files under `dist/` is supported, as shown
by `examples/hello-plugin`.
Only package-relative imports are resolved this way. Bare specifiers such as
`react`, `lodash` or any dependency expected from `node_modules` are not
resolved by the host at runtime. Third-party dependencies must be bundled into
the plugin output or vendored as relative files shipped inside the plugin
package.
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.
## Plugin-Owned Storage
Use `ctx.storage` for state owned by the plugin itself: internal counters, flags,
preferences, small caches and host-facing settings. This is the canonical place
for plugin-owned state because the host can scope and persist it outside the
user's project files.
```ts
import type { ActivateContext } from "@idea/plugin-sdk";
const ACTIVATION_COUNT_KEY = "helloPlugin.activationCount";
export async function activate(ctx: ActivateContext): Promise<void> {
const current = await ctx.storage?.get<number>(ACTIVATION_COUNT_KEY);
const next = typeof current === "number" ? current + 1 : 1;
await ctx.storage?.set(ACTIVATION_COUNT_KEY, next);
ctx.logger.info("activation count", { next });
}
```
Do not write plugin-internal state into `.ideai/*` or other project files by
default. Use workspace files and config-document helpers only when the file is
project-owned content or project-owned configuration that the user expects to
see, review and version with the project.
## 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`. These APIs are for project-owned files:
source files, generated reports or user-visible artifacts. Use `ctx.storage`
instead for plugin-owned counters, flags, preferences and caches.
```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("hello-plugin-report.txt", "hello\n", project.id);
const file = await workspace.readTextFile("hello-plugin-report.txt", project.id);
const listing = await workspace.listDirectory(".", 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
project-owned configuration file without reimplementing parsing and
serialization. Do not use project config documents as the default persistence
mechanism for plugin-internal state; use `ctx.storage` for that.
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: "tooling.config.json"
});
await ctx.services?.config.updateDocument({
path: "tooling.config.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,60 @@
# 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 });
}
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`, `pluginDisplayName`, `version`: host-provided identity.
- `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.
## 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,154 @@
# Commands And Feedback
This document is the normative SDK contract for plugin commands that may launch
background work.
## Contract
Every human menu click follows this sequence:
```text
menu item -> command id -> registered command handler -> optional task -> feedback surfaces
```
- A manifest menu item declares a `command` id; it does not run tools directly.
- The command handler is the only place that decides whether work should start.
- 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. Use logs, Work/task state or plugin-owned UI/files when
a human needs visible feedback today.
## 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` 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 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
```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.

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

@ -0,0 +1,85 @@
# 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
});
```
## 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.

68
docs/manifest.md Normal file
View File

@ -0,0 +1,68 @@
# Manifest
Every plugin package has an `idea-plugin.json` file at the archive root.
```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.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.
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.
## 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,76 @@
# 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.

68
docs/services.md Normal file
View File

@ -0,0 +1,68 @@
# 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.
## 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).

46
docs/windows.md Normal file
View File

@ -0,0 +1,46 @@
# 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" }
});
```
## Contract
- `layoutType` must match a layout type declared by the calling plugin.
- 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 () => {
return ctx.services?.windows.open({
layoutType: "hello-plugin.dashboard",
state: { source: "menu" }
});
});
```

View File

@ -6,8 +6,14 @@ 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; - 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.
@ -15,13 +21,14 @@ The source is intentionally split across multiple TypeScript modules:
- `src/index.ts` is the manifest entrypoint and imports relative ESM modules; - `src/index.ts` is the manifest entrypoint and imports relative ESM modules;
- `src/constants.ts` owns shared command/layout identifiers; - `src/constants.ts` owns shared command/layout identifiers;
- `src/core/layout.ts` and `src/core/workspace.ts` hold feature logic. - `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`. - `src/core/storage.ts` keeps plugin-owned counters and flags in `ctx.storage`.
The build uses plain `tsc`; it does not bundle the plugin into one file. The archive includes all 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 compiled `dist/**/*.js` files so IdeA can load `dist/index.js` and serve its package-relative imports
through `idea-plugin://`. Runtime imports from `node_modules` are outside this contract: vendor them through `idea-plugin://`. React and ReactDOM are peer dependencies resolved to the host instance at
as relative files or bundle them into the plugin output before packaging. 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
@ -42,7 +49,16 @@ During activation the plugin logs:
- 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

@ -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

@ -1,17 +0,0 @@
import type { PluginLayoutProps } from "@idea/plugin-sdk";
let hasLoggedFirstLayoutRender = false;
export 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";
}

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

@ -1,6 +1,19 @@
import type { ActivateContext } from "@idea/plugin-sdk"; import type { ActivateContext, CommandTaskStatus } from "@idea/plugin-sdk";
import { STORAGE_KEYS } from "../constants.js"; 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> { export async function useWorkspaceSdk(ctx: ActivateContext): Promise<void> {
const workspace = ctx.services?.workspace; const workspace = ctx.services?.workspace;
if (!workspace) return; if (!workspace) return;
@ -44,14 +57,18 @@ export async function useWorkspaceSdk(ctx: ActivateContext): Promise<void> {
messages: diagnostic?.messages messages: diagnostic?.messages
}); });
const watch = await workspace.watch(".ideai", (event) => { try {
ctx.logger.info("workspace watch event", { const watch = await workspace.watch(".ideai", (event) => {
path: event.path, ctx.logger.info("workspace watch event", {
kind: event.kind, path: event.path,
operation: event.operation kind: event.kind,
}); operation: event.operation
}, project.id); });
ctx.subscriptions.push(watch); }, project.id);
ctx.subscriptions.push(watch);
} catch (error) {
ctx.logger.info("workspace watch unavailable", { error });
}
const events = await ctx.services?.events.subscribe( const events = await ctx.services?.events.subscribe(
{ {
@ -70,14 +87,42 @@ export async function useWorkspaceSdk(ctx: ActivateContext): Promise<void> {
} }
); );
if (events) ctx.subscriptions.push(events); 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); const ownerAgentId = await ctx.storage?.get<string>(STORAGE_KEYS.ownerAgentId);
if (!ownerAgentId) { if (!ownerAgentId) {
ctx.logger.info("command task example skipped: no owner agent configured"); const feedback = {
return; 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 task = await ctx.services?.tasks.runCommand({ const task = await ctx.services.tasks.runCommand({
projectId: project.id, projectId: project.id,
ownerAgentId, ownerAgentId,
label: "Hello plugin command", label: "Hello plugin command",
@ -87,12 +132,17 @@ export async function useWorkspaceSdk(ctx: ActivateContext): Promise<void> {
recordOnly: true recordOnly: true
}); });
if (task) { const status = await ctx.services.tasks.getCommandStatus(task.taskId);
const status = await ctx.services?.tasks.getCommandStatus(task.taskId); const feedback = {
ctx.logger.info("command task launched", { status: "launched",
taskId: task.taskId, taskId: task.taskId,
state: status?.state ?? task.state, state: status?.state ?? task.state,
exitCode: status?.exitCode ?? task.exitCode 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

@ -2,7 +2,7 @@ import type { ActivateContext, IdeAPluginModule } from "@idea/plugin-sdk";
import { COMMAND_ID, LAYOUT_TYPE } from "./constants.js"; import { COMMAND_ID, LAYOUT_TYPE } from "./constants.js";
import { HelloWorldLayout } from "./core/layout.js"; import { HelloWorldLayout } from "./core/layout.js";
import { initializePluginStorage, recordCommandRun } from "./core/storage.js"; import { initializePluginStorage, recordCommandRun } from "./core/storage.js";
import { useWorkspaceSdk } from "./core/workspace.js"; import { runHelloCommandTask, useWorkspaceSdk } from "./core/workspace.js";
export function activate(ctx: ActivateContext): void { export function activate(ctx: ActivateContext): void {
ctx.logger.info("activating hello-plugin", { ctx.logger.info("activating hello-plugin", {
@ -14,8 +14,19 @@ export function activate(ctx: ActivateContext): void {
const commandDisposable = ctx.commands?.registerCommand(COMMAND_ID, async () => { const commandDisposable = ctx.commands?.registerCommand(COMMAND_ID, async () => {
const commandRunCount = await recordCommandRun(ctx); const commandRunCount = await recordCommandRun(ctx);
ctx.logger.info("command executed", { commandId: COMMAND_ID, commandRunCount }); const feedback = await runHelloCommandTask(ctx);
return "hello-world"; 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) {

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

@ -43,6 +43,7 @@ export type {
IdeAPluginModule, IdeAPluginModule,
JsonValue, JsonValue,
LayoutRegistry, LayoutRegistry,
OpenPluginWindowOptions,
PluginLogger, PluginLogger,
PluginLayoutAvailability, PluginLayoutAvailability,
PluginLayoutComponent, PluginLayoutComponent,
@ -52,6 +53,7 @@ export type {
PluginLayoutState, PluginLayoutState,
PluginServices, PluginServices,
PluginStorage, PluginStorage,
PluginWindow,
ProjectConvention, ProjectConvention,
ProjectModule, ProjectModule,
ProjectStructure, ProjectStructure,
@ -82,5 +84,6 @@ export type {
WorkspaceTextFile, WorkspaceTextFile,
WorkspaceWatch, WorkspaceWatch,
WorkspaceWatchEvent, WorkspaceWatchEvent,
WorkspaceWatchHandler WorkspaceWatchHandler,
WindowService
} from "./runtime.js"; } from "./runtime.js";

View File

@ -1,3 +1,5 @@
import type { ComponentType, ReactNode } from "react";
export interface ActivateContext { export interface ActivateContext {
pluginId: string; pluginId: string;
logger: PluginLogger; logger: PluginLogger;
@ -47,7 +49,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. */
@ -64,9 +66,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. */
@ -87,6 +88,7 @@ export interface PluginServices {
events: EventService; events: EventService;
config: ConfigDocumentService; config: ConfigDocumentService;
terminal: TerminalService; terminal: TerminalService;
windows: WindowService;
} }
export interface WorkspaceProject { export interface WorkspaceProject {
@ -128,6 +130,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. */
@ -219,9 +222,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;
@ -245,7 +258,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;
@ -257,7 +274,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;
@ -265,9 +287,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;
@ -479,7 +509,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>;
@ -534,3 +572,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>;
}