From 15f930dd3bb036709c40418e0426fdb4adf6ce42 Mon Sep 17 00:00:00 2001 From: Blomios Date: Tue, 4 Aug 2026 00:39:25 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(runtime):=20r=C3=A9sout=20react/react-?= =?UTF-8?q?dom=20vers=20l'instance=20host=20pour=20les=20layouts=20plugin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- examples/hello-plugin/package-lock.json | 75 +++++++++++++++++++++++ examples/hello-plugin/package.json | 11 +++- examples/hello-plugin/src/core/layout.ts | 17 ----- examples/hello-plugin/src/core/layout.tsx | 58 ++++++++++++++++++ examples/hello-plugin/src/index.ts | 10 +++ examples/hello-plugin/tsconfig.build.json | 3 +- examples/hello-plugin/tsconfig.json | 2 + package-lock.json | 65 ++++++++++++++++++++ package.json | 10 ++- src/index.ts | 5 +- src/runtime.ts | 39 ++++++++++-- 11 files changed, 270 insertions(+), 25 deletions(-) delete mode 100644 examples/hello-plugin/src/core/layout.ts create mode 100644 examples/hello-plugin/src/core/layout.tsx diff --git a/examples/hello-plugin/package-lock.json b/examples/hello-plugin/package-lock.json index 9935509..0514481 100644 --- a/examples/hello-plugin/package-lock.json +++ b/examples/hello-plugin/package-lock.json @@ -9,6 +9,16 @@ "version": "0.1.0", "dependencies": { "@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", "license": "MIT", "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" + }, + "peerDependencies": { + "react": "^18.2.0 || ^19.0.0", + "react-dom": "^18.2.0 || ^19.0.0" } }, "node_modules/@idea/plugin-sdk": { "resolved": "../..", "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" } } } diff --git a/examples/hello-plugin/package.json b/examples/hello-plugin/package.json index 937f370..b8ae5b5 100644 --- a/examples/hello-plugin/package.json +++ b/examples/hello-plugin/package.json @@ -9,6 +9,15 @@ }, "dependencies": { "@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" } } - diff --git a/examples/hello-plugin/src/core/layout.ts b/examples/hello-plugin/src/core/layout.ts deleted file mode 100644 index 08fb697..0000000 --- a/examples/hello-plugin/src/core/layout.ts +++ /dev/null @@ -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"; -} diff --git a/examples/hello-plugin/src/core/layout.tsx b/examples/hello-plugin/src/core/layout.tsx new file mode 100644 index 0000000..09c522b --- /dev/null +++ b/examples/hello-plugin/src/core/layout.tsx @@ -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) + : {}; + }, [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 ( +
+
+

Hello Plugin

+

+ React layout rendered by IdeA host React. +

+
+
+
+
Project
+
{props.projectId}
+
+
+
Layout
+
{props.layoutType}
+
+
+ +
+ ); +} diff --git a/examples/hello-plugin/src/index.ts b/examples/hello-plugin/src/index.ts index 007edea..4484e07 100644 --- a/examples/hello-plugin/src/index.ts +++ b/examples/hello-plugin/src/index.ts @@ -15,7 +15,17 @@ export function activate(ctx: ActivateContext): void { const commandDisposable = ctx.commands?.registerCommand(COMMAND_ID, async () => { const commandRunCount = await recordCommandRun(ctx); 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; }); diff --git a/examples/hello-plugin/tsconfig.build.json b/examples/hello-plugin/tsconfig.build.json index 9121acc..2e6cdef 100644 --- a/examples/hello-plugin/tsconfig.build.json +++ b/examples/hello-plugin/tsconfig.build.json @@ -14,6 +14,7 @@ } }, "include": [ - "src/**/*.ts" + "src/**/*.ts", + "src/**/*.tsx" ] } diff --git a/examples/hello-plugin/tsconfig.json b/examples/hello-plugin/tsconfig.json index 27c3a9e..b5da265 100644 --- a/examples/hello-plugin/tsconfig.json +++ b/examples/hello-plugin/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "declaration": false, "declarationMap": false, + "jsx": "react-jsx", "noEmit": true, "rootDir": "../..", "paths": { @@ -13,6 +14,7 @@ }, "include": [ "src/**/*.ts", + "src/**/*.tsx", "../../src/**/*.ts" ] } diff --git a/package-lock.json b/package-lock.json index ffbe024..6e83a4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,74 @@ "version": "0.1.0", "license": "MIT", "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" + }, + "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": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/package.json b/package.json index de67eac..5fde6fc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@idea/plugin-sdk", "version": "0.1.0", - "description": "Minimal public TypeScript SDK for IdeA plugins.", + "description": "Public TypeScript SDK for IdeA plugins.", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -29,7 +29,15 @@ "sdk" ], "license": "MIT", + "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", "typescript": "^5.5.0" } } diff --git a/src/index.ts b/src/index.ts index be8b304..fb39ad6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,6 +43,7 @@ export type { IdeAPluginModule, JsonValue, LayoutRegistry, + OpenPluginWindowOptions, PluginLogger, PluginLayoutAvailability, PluginLayoutComponent, @@ -52,6 +53,7 @@ export type { PluginLayoutState, PluginServices, PluginStorage, + PluginWindow, ProjectConvention, ProjectModule, ProjectStructure, @@ -82,5 +84,6 @@ export type { WorkspaceTextFile, WorkspaceWatch, WorkspaceWatchEvent, - WorkspaceWatchHandler + WorkspaceWatchHandler, + WindowService } from "./runtime.js"; diff --git a/src/runtime.ts b/src/runtime.ts index 88286fe..e1728c7 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -1,3 +1,5 @@ +import type { ComponentType, ReactNode } from "react"; + export interface ActivateContext { pluginId: string; logger: PluginLogger; @@ -47,7 +49,7 @@ export interface PluginStorage { export type PluginLayoutState = JsonValue | undefined; export type PluginLayoutAvailability = "available"; -export type PluginLayoutRenderResult = unknown; +export type PluginLayoutRenderResult = ReactNode; export interface PluginLayoutProps { /** Project currently hosting this layout cell. */ @@ -64,9 +66,8 @@ export interface PluginLayoutProps = ( - props: PluginLayoutProps, -) => PluginLayoutRenderResult; +export type PluginLayoutComponent = + ComponentType>; export interface PluginLayoutDefinition { /** Must match a layout `type` declared in this plugin's manifest. */ @@ -87,6 +88,7 @@ export interface PluginServices { events: EventService; config: ConfigDocumentService; terminal: TerminalService; + windows: WindowService; } export interface WorkspaceProject { @@ -570,3 +572,32 @@ export interface TerminalService { /** Kills a PTY by id. */ close(sessionId: string): Promise; } + +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; +} From 31925dc1ce0c4d8209d75dcc2a85ab8beb9ea26b Mon Sep 17 00:00:00 2001 From: Blomios Date: Tue, 4 Aug 2026 00:39:30 +0200 Subject: [PATCH 2/2] =?UTF-8?q?docs(sdk):=20=C3=A9clate=20et=20=C3=A9tend?= =?UTF-8?q?=20la=20documentation=20SDK=20par=20sujet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 466 ++------------------------------ docs/activation-context.md | 60 ++++ docs/commands-and-feedback.md | 4 +- docs/layouts-react.md | 85 ++++++ docs/manifest.md | 68 +++++ docs/menus.md | 56 ++++ docs/packaging-distribution.md | 76 ++++++ docs/services.md | 68 +++++ docs/windows.md | 46 ++++ examples/hello-plugin/README.md | 15 +- 10 files changed, 489 insertions(+), 455 deletions(-) create mode 100644 docs/activation-context.md create mode 100644 docs/layouts-react.md create mode 100644 docs/manifest.md create mode 100644 docs/menus.md create mode 100644 docs/packaging-distribution.md create mode 100644 docs/services.md create mode 100644 docs/windows.md diff --git a/README.md b/README.md index e239cd7..b21e413 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,25 @@ # 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`; -- public runtime types for plugin modules exposing `activate(ctx)`; -- plugin-owned persistent storage through `ctx.storage`; -- a stable `ctx.services` facade for workspace, background task and terminal operations; -- public workspace file APIs for reading, writing, listing, stat and path resolution; -- a bounded generic project-structure query API; -- public command-task APIs for launching and tracking generic tools; -- public external-toolchain diagnostics for executables, env vars and files; -- public best-effort event subscriptions and workspace watch; -- public structured config-document helpers for JSON documents; -- a lightweight manifest validator; -- a minimal `examples/hello-plugin` plugin. +## Start Here -## Command And Feedback Contract +- [Manifest](docs/manifest.md): required fields, capabilities and contribution ids. +- [Activation And Context](docs/activation-context.md): `activate(ctx)`, lifecycle, logging, storage and subscriptions. +- [Menus](docs/menus.md): top-level menus, native menu insertion and command registration. +- [Commands And Feedback](docs/commands-and-feedback.md): command return values, skipped work and background tasks. +- [Layouts With React](docs/layouts-react.md): React/JSX/hooks layout authoring with host-provided React. +- [Windows](docs/windows.md): opening plugin layouts in detached OS windows. +- [Services](docs/services.md): workspace, tasks, tooling, events, config, terminal and windows facades. +- [Packaging And Distribution](docs/packaging-distribution.md): build output, archive layout and dependency rules. -Plugin menu actions follow one public contract: - -```text -menu click -> registered command handler -> optional background task -> feedback surfaces -``` - -A menu item only names a command. The registered command handler owns all -precondition checks, task launch decisions and feedback. If a command -cannot or should not start work, return a small structured result such as -`{ status: "skipped", reason, message }` and log the same reason. That return -value is useful for programmatic callers, agents and future host surfaces; the -current human menu-click UI does not guarantee that handler return values are -shown to the user. Do not create a background task just to represent a skipped -command. - -Use `ctx.services.tasks.runCommand()` only after required preconditions are -true: a focused project exists, the plugin has the `tooling` capability, required -executables/files/env are present, and a real `ownerAgentId` is available when -the result belongs to an agent workflow. `ownerAgentId` controls Work ownership, -cancellation and completion delivery; it must not be a placeholder in production -plugin code. - -`recordOnly: true` records completion without waking the owner agent. It is not a -silent mode and it does not hide the task from surfaces that show Work state or -background-task events. If no task is launched, the baseline feedback surfaces -are the command result for programmatic callers and plugin logs; human-visible UI -feedback requires a host-supported surface, a real background task, or a -plugin-owned UI/file surface. - -Canonical rules and examples live in -[`docs/commands-and-feedback.md`](docs/commands-and-feedback.md). +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 @@ -63,415 +33,15 @@ npm install npm run build ``` -## Typecheck the example +## Validate The Example ```sh npm run typecheck:examples -``` - -## Build the installable hello plugin archive - -```sh npm run package:hello-plugin ``` -The archive is written to: +The example archive is written to: ```text 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 { - const current = await ctx.storage?.get(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 { - 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 { - 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: hosts may -reject it until workspace watching is implemented, and plugins must treat setup -failure as non-fatal. Plugins should also 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 { - 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 { - 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 { - 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 { - 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. diff --git a/docs/activation-context.md b/docs/activation-context.md new file mode 100644 index 0000000..e8217bc --- /dev/null +++ b/docs/activation-context.md @@ -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. diff --git a/docs/commands-and-feedback.md b/docs/commands-and-feedback.md index 1959faa..51be5d5 100644 --- a/docs/commands-and-feedback.md +++ b/docs/commands-and-feedback.md @@ -27,8 +27,8 @@ menu item -> command id -> registered command handler -> optional task -> feedba Check preconditions before calling `runCommand()`: -- `ctx.services` exists. Plugins need the `tooling` capability for the service - facade. +- `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, diff --git a/docs/layouts-react.md b/docs/layouts-react.md new file mode 100644 index 0000000..e823ee8 --- /dev/null +++ b/docs/layouts-react.md @@ -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 ( + + ); +} +``` + +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. diff --git a/docs/manifest.md b/docs/manifest.md new file mode 100644 index 0000000..3badddf --- /dev/null +++ b/docs/manifest.md @@ -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)); +``` diff --git a/docs/menus.md b/docs/menus.md new file mode 100644 index 0000000..19b638c --- /dev/null +++ b/docs/menus.md @@ -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. diff --git a/docs/packaging-distribution.md b/docs/packaging-distribution.md new file mode 100644 index 0000000..1dc0319 --- /dev/null +++ b/docs/packaging-distribution.md @@ -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. diff --git a/docs/services.md b/docs/services.md new file mode 100644 index 0000000..bbe2969 --- /dev/null +++ b/docs/services.md @@ -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). diff --git a/docs/windows.md b/docs/windows.md new file mode 100644 index 0000000..8bd3b62 --- /dev/null +++ b/docs/windows.md @@ -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" } + }); +}); +``` diff --git a/examples/hello-plugin/README.md b/examples/hello-plugin/README.md index 00633e0..5e8c172 100644 --- a/examples/hello-plugin/README.md +++ b/examples/hello-plugin/README.md @@ -10,7 +10,10 @@ It exercises the current plugin primitives end to end: `{ 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 as `hello-world`. +- 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. @@ -18,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/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`. 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://`. Runtime imports from `node_modules` are outside this contract: vendor them -as relative files or bundle them into the plugin output before packaging. +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 npm run typecheck:examples @@ -46,7 +50,8 @@ During activation the plugin logs: - successful registration of the `hello-plugin.hello-world` layout; - availability of the workspace service from the `tooling` runtime capability; - best-effort workspace watch setup, including a non-fatal log when unavailable; -- the first layout render, including project/node identifiers. +- 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