feat(sdk,plugins): ajoute capability tooling et services runtime publics (workspace/terminal/tasks)

- capability manifeste tooling supportée backend + transport runtime catalog
- ctx.services publique côté SDK/runtime, gated par tooling
- surface honnête: workspace, terminal, et tasks en observation/contrôle uniquement
- docs/exemple/tests mis à jour
- QA PASS sur feature/sdk-plugin-tooling-build-debug-surface
This commit is contained in:
2026-08-02 00:31:15 +02:00
parent efaeb31a38
commit 033e9a86d5
21 changed files with 748 additions and 10 deletions

View File

@ -6,6 +6,7 @@ This first version intentionally stays small:
- public manifest types for `idea-plugin.json`;
- public runtime types for plugin modules exposing `activate(ctx)`;
- a stable `ctx.services` facade for workspace, background task and terminal operations;
- a lightweight manifest validator;
- a minimal `examples/hello-plugin` plugin.
@ -69,6 +70,42 @@ export function activate(ctx: ActivateContext): void {
}
```
## 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"));
}
```
Current terminal scope is intentionally minimal: it opens or reattaches a shell
PTY, writes bytes, resizes, detaches and closes. The background task service is
observation/control only in this SDK version: `list`, `getStatus`, `attachOutput`,
`cancel` and `retry` operate on existing tasks visible through IdeA's Work read
model. Starting new background tasks is not part of the public plugin API in this
lot.
Declare the additive `tooling` capability to receive `ctx.services` at runtime:
```json
{
"capabilities": ["ui", "tooling"]
}
```
## Manifest Validation
```ts

View File

@ -8,6 +8,7 @@ It exercises the current plugin primitives end to end:
- menu entry: `hello-plugin`;
- command: `hello-plugin`, returning `hello-world`;
- layout contribution: `hello-plugin.hello-world`, rendered as `hello-world`.
- tooling capability: logs the focused workspace project when `ctx.services` is available.
```sh
npm run typecheck:examples
@ -25,6 +26,7 @@ During activation the plugin logs:
- whether the command and layout runtime registries are available;
- successful registration of the `hello-plugin` command;
- successful registration of the `hello-plugin.hello-world` layout;
- availability of the workspace service from the `tooling` runtime capability;
- the first layout render, including project/node identifiers.
These messages are intentionally small and stable so installation, bundle import, activation and

View File

@ -11,7 +11,8 @@
},
"trustLevel": "full",
"capabilities": [
"ui"
"ui",
"tooling"
],
"contributes": {
"menus": [

View File

@ -71,6 +71,13 @@ export function activate(ctx: ActivateContext): void {
} else {
ctx.logger.warn("layout registry unavailable", { layoutType: LAYOUT_TYPE });
}
void ctx.services?.workspace.getCurrentProject().then((project) => {
ctx.logger.info("workspace service available", {
projectId: project?.id ?? null,
hasProjectRoot: Boolean(project?.root)
});
});
}
const plugin: IdeAPluginModule = {

View File

@ -17,7 +17,19 @@ export type {
CommandDisposable,
CommandHandler,
CommandRegistry,
BackgroundTaskOutputAttachment,
BackgroundTaskRetryResult,
BackgroundTaskService,
BackgroundTaskStatus,
IdeAPluginModule,
PluginLogger,
PluginStorage
PluginServices,
PluginStorage,
TerminalOpenOptions,
TerminalReattachOptions,
TerminalReattachResult,
TerminalService,
TerminalSession,
WorkspaceProject,
WorkspaceService
} from "./runtime.js";

View File

@ -59,8 +59,8 @@ function validateCapabilities(value, errors) {
return;
}
value.forEach((capability, index) => {
if (capability !== "ui" && capability !== "mcp") {
errors.push(`capabilities[${index}] must be "ui" or "mcp"`);
if (capability !== "ui" && capability !== "mcp" && capability !== "tooling") {
errors.push(`capabilities[${index}] must be "ui", "mcp" or "tooling"`);
}
});
}

View File

@ -17,7 +17,7 @@ export interface IdeAPluginManifest {
};
}
export type IdeAPluginCapability = "ui" | "mcp";
export type IdeAPluginCapability = "ui" | "mcp" | "tooling";
export interface IdeAPluginEngineConstraints {
idea?: string;
@ -145,8 +145,8 @@ function validateCapabilities(value: unknown, errors: string[]): void {
}
value.forEach((capability, index) => {
if (capability !== "ui" && capability !== "mcp") {
errors.push(`capabilities[${index}] must be "ui" or "mcp"`);
if (capability !== "ui" && capability !== "mcp" && capability !== "tooling") {
errors.push(`capabilities[${index}] must be "ui", "mcp" or "tooling"`);
}
});
}

View File

@ -4,6 +4,12 @@ export interface ActivateContext {
subscriptions: CommandDisposable[];
commands?: CommandRegistry;
storage?: PluginStorage;
/**
* Stable public service facade for plugins that need workspace, background
* task, or terminal operations. This intentionally does not expose IdeA's
* internal runtime/gateway objects.
*/
services?: PluginServices;
}
export interface IdeAPluginModule {
@ -34,3 +40,103 @@ export interface PluginStorage {
delete(key: string): Promise<void>;
}
export interface PluginServices {
workspace: WorkspaceService;
tasks: BackgroundTaskService;
terminal: TerminalService;
}
export interface WorkspaceProject {
id: string;
name: string;
root: string;
}
export interface WorkspaceService {
/** Returns the currently focused project, or null when no project is active. */
getCurrentProject(): Promise<WorkspaceProject | null>;
/** Returns the root path for the given project or for the current project. */
getProjectRoot(projectId?: string): Promise<string>;
/** Reads IdeA's shared project context for the given or current project. */
readProjectContext(projectId?: string): Promise<string>;
/** Updates IdeA's shared project context for the given or current project. */
updateProjectContext(content: string, projectId?: string): Promise<void>;
}
export interface BackgroundTaskStatus {
taskId: string;
ownerAgentId: string;
projectId: string;
kind: string;
status: "pending" | "running" | "completed" | "failed" | "cancelled" | "delivered";
exitCode: number | null;
summary: string | null;
stdoutTail: string | null;
stderrTail: string | null;
updatedAtMs: number;
}
export interface BackgroundTaskOutputAttachment {
taskId: string;
scrollback: Uint8Array;
live: boolean;
detach(): void;
}
export interface BackgroundTaskRetryResult {
/** Present when the host reports the replacement task id. */
taskId?: string;
}
export interface BackgroundTaskService {
/** Lists background tasks visible in the project work-state read model. */
list(projectId?: string): Promise<BackgroundTaskStatus[]>;
/** Reads one task status from the project work-state read model. */
getStatus(taskId: string, projectId?: string): Promise<BackgroundTaskStatus | null>;
/** Attaches to retained/live output for a task. */
attachOutput(
taskId: string,
onData: (bytes: Uint8Array) => void,
): Promise<BackgroundTaskOutputAttachment>;
/** Cancels a pending/running task. */
cancel(taskId: string): Promise<void>;
/** Retries a failed/cancelled task; future hosts may return the new task id. */
retry(taskId: string): Promise<BackgroundTaskRetryResult>;
}
export interface TerminalOpenOptions {
cwd?: string;
rows?: number;
cols?: number;
onData?: (bytes: Uint8Array) => void;
}
export interface TerminalReattachOptions {
onData?: (bytes: Uint8Array) => void;
}
export interface TerminalSession {
readonly sessionId: string;
write(data: Uint8Array): Promise<void>;
resize(rows: number, cols: number): Promise<void>;
detach(): void;
close(): Promise<void>;
}
export interface TerminalReattachResult {
session: TerminalSession;
scrollback: Uint8Array;
}
export interface TerminalService {
/**
* Opens a shell PTY in the requested/current project directory. This MVP is a
* terminal control surface, not a command runner; use tasks for build/test
* commands that should be tracked in the Work panel.
*/
open(options?: TerminalOpenOptions): Promise<TerminalSession>;
/** Reattaches to an already-running PTY and returns retained scrollback. */
reattach(sessionId: string, options?: TerminalReattachOptions): Promise<TerminalReattachResult>;
/** Kills a PTY by id. */
close(sessionId: string): Promise<void>;
}