Files
IdeaSDK/src/runtime.ts
Blomios 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

604 lines
19 KiB
TypeScript

import type { ComponentType, ReactNode } from "react";
export interface ActivateContext {
pluginId: string;
logger: PluginLogger;
subscriptions: CommandDisposable[];
commands?: CommandRegistry;
layouts?: LayoutRegistry;
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 {
activate(ctx: ActivateContext): void | Promise<void>;
deactivate?(): void | Promise<void>;
}
export interface PluginLogger {
debug(message: string, ...args: unknown[]): void;
info(message: string, ...args: unknown[]): void;
warn(message: string, ...args: unknown[]): void;
error(message: string, ...args: unknown[]): void;
}
export type CommandHandler = (...args: unknown[]) => unknown | Promise<unknown>;
export interface CommandRegistry {
registerCommand(commandId: string, handler: CommandHandler): CommandDisposable;
}
export interface CommandDisposable {
dispose(): void;
}
export interface PluginStorage {
/**
* Plugin-owned persistent state. Use this for internal counters, flags,
* preferences and caches that should not be written into the user's project.
*/
get<T = unknown>(key: string): Promise<T | undefined>;
set<T = unknown>(key: string, value: T): Promise<void>;
delete(key: string): Promise<void>;
}
export type PluginLayoutState = JsonValue | undefined;
export type PluginLayoutAvailability = "available";
export type PluginLayoutRenderResult = ReactNode;
export interface PluginLayoutProps<TState extends PluginLayoutState = PluginLayoutState> {
/** Project currently hosting this layout cell. */
projectId: string;
/** Stable layout node id for this cell instance. */
nodeId: string;
/** Layout contribution type declared in `idea-plugin.json`. */
layoutType: string;
/** Opaque JSON-serializable state persisted by the host for this cell. */
state: TState;
/** Replaces the opaque state for this cell. Values must be JSON-serializable. */
setState(next: TState): void;
/** Present layouts are only mounted when available; fallback UI is host-owned. */
availability: PluginLayoutAvailability;
}
export type PluginLayoutComponent<TState extends PluginLayoutState = PluginLayoutState> =
ComponentType<PluginLayoutProps<TState>>;
export interface PluginLayoutDefinition<TState extends PluginLayoutState = PluginLayoutState> {
/** Must match a layout `type` declared in this plugin's manifest. */
type: string;
component: PluginLayoutComponent<TState>;
}
export interface LayoutRegistry {
register<TState extends PluginLayoutState = PluginLayoutState>(
definition: PluginLayoutDefinition<TState>,
): CommandDisposable;
}
export interface PluginServices {
workspace: WorkspaceService;
tasks: BackgroundTaskService;
tooling: ToolingService;
events: EventService;
config: ConfigDocumentService;
terminal: TerminalService;
windows: WindowService;
}
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>;
/**
* Resolves and normalizes a plugin-visible path under the project root.
* Rejects absolute paths, `..`, empty segments and other paths the host
* considers outside the workspace sandbox.
*/
resolvePath(path: string, projectId?: string): Promise<WorkspaceResolvedPath>;
/** Reads a UTF-8 text file under the project root. */
readTextFile(path: string, projectId?: string): Promise<WorkspaceTextFile>;
/** Reads raw bytes from a file under the project root. */
readBinaryFile(path: string, projectId?: string): Promise<WorkspaceBinaryFile>;
/** Writes UTF-8 text under the project root using the host's controlled write path. */
writeTextFile(path: string, content: string, projectId?: string): Promise<void>;
/** Writes raw bytes under the project root using the host's controlled write path. */
writeBinaryFile(path: string, bytes: Uint8Array, projectId?: string): Promise<void>;
/** Lists one directory under the project root. Defaults to the workspace root. */
listDirectory(path?: string, projectId?: string): Promise<WorkspaceDirectoryListing>;
/**
* Returns basic metadata. Missing paths resolve to `{ exists: false }`; invalid
* paths and permission errors reject.
*/
stat(path: string, projectId?: string): Promise<WorkspaceStat>;
/**
* 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.
* Plugins must treat watch setup as best-effort and non-fatal.
*/
watch(path: string, handler: WorkspaceWatchHandler, projectId?: string): Promise<WorkspaceWatch>;
/** Queries a bounded, generic project structure read model. */
queryStructure(query?: WorkspaceStructureQuery): Promise<ProjectStructure>;
}
export interface WorkspaceResolvedPath {
projectId: string;
root: string;
path: string;
}
export interface WorkspaceTextFile {
path: string;
content: string;
}
export interface WorkspaceBinaryFile {
path: string;
bytes: Uint8Array;
}
export interface WorkspaceDirEntry {
name: string;
path: string;
isDir: boolean;
}
export interface WorkspaceDirectoryListing {
path: string;
entries: WorkspaceDirEntry[];
}
export interface WorkspaceStat {
path: string;
exists: boolean;
isFile: boolean;
isDir: boolean;
len: number | null;
}
export interface WorkspaceWatchEvent {
path: string;
kind: "created" | "modified" | "deleted" | "renamed" | "unknown";
operation: string;
projectId: string;
}
export type WorkspaceWatchHandler = (event: WorkspaceWatchEvent) => void;
export interface WorkspaceWatch {
dispose(): void;
}
export interface WorkspaceStructureQuery {
projectId?: string;
path?: string;
maxDepth?: number;
maxEntries?: number;
}
export type ProjectStructureEntryKind = "file" | "directory";
export interface ProjectStructureEntry {
path: string;
name: string;
kind: ProjectStructureEntryKind;
}
export interface ProjectConvention {
id: string;
markerPath: string;
}
export interface ProjectModule {
path: string;
markerPath: string;
conventionId: string;
}
export interface ProjectStructure {
projectId: string;
rootPath: string;
entries: ProjectStructureEntry[];
conventions: ProjectConvention[];
modules: ProjectModule[];
truncated: boolean;
}
export interface BackgroundTaskStatus {
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;
projectId: 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";
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 RunCommandTaskOptions {
/** Project that owns the command workspace. Defaults to the focused project. */
projectId?: string;
/**
* 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;
/** Human-facing label shown in Work. Defaults to the command line. */
label?: string;
/** Executable to run. Arguments are passed separately, without shell parsing. */
command: string;
/** Arguments passed to the executable. */
args?: string[];
/** Relative working directory under the project root. Defaults to the root. */
cwd?: string;
/** Extra environment variables for the command. */
env?: Record<string, string> | Array<[string, string]>;
/**
* 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;
/** Optional absolute deadline, epoch milliseconds. */
deadlineMs?: number;
}
export interface CommandTaskStatus {
taskId: string;
/**
* Agent that owns this command task. The host uses it for correlation,
* cancellation and completion delivery.
*/
ownerAgentId: string;
projectId: 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";
exitCode: number | null;
summary: string | null;
stdoutTail: string | null;
stderrTail: string | null;
createdAtMs: number;
updatedAtMs: number;
}
export interface ToolRequirement {
/** Stable id chosen by the plugin for this executable prerequisite. */
id: string;
/** Executable name or path to probe. */
executable: string;
/** Version/diagnostic arguments. Defaults host-side to `--version`. */
versionArgs?: string[];
/** Whether this tool must pass for the whole diagnostic to be ok. */
required?: boolean;
/** Extra environment variables for this probe. */
env?: Record<string, string> | Array<[string, string]>;
}
export interface EnvRequirement {
/** Environment variable name. */
name: string;
/** Whether the variable must be present and match. */
required?: boolean;
/** Optional exact expected value. */
equals?: string;
}
export interface FileRequirement {
/** Relative workspace path. */
path: string;
/** Whether the path must exist and match `kind`. */
required?: boolean;
/** Expected workspace path kind. */
kind?: "file" | "directory" | "any";
}
export interface ToolchainDiagnosticRequest {
/** Project to inspect. Defaults to the focused project. */
projectId?: string;
/** Relative working directory under the project root. Defaults to the root. */
cwd?: string;
/** Executable probes to run. */
tools?: ToolRequirement[];
/** Environment variable prerequisites to inspect. */
env?: EnvRequirement[];
/** Workspace file prerequisites to validate. */
files?: FileRequirement[];
}
export interface ToolchainDiagnostic {
projectId: string;
cwd: string;
ok: boolean;
tools: ToolDiagnostic[];
env: EnvDiagnostic[];
files: FileDiagnostic[];
messages: DiagnosticMessage[];
}
export interface ToolDiagnostic {
id: string;
executable: string;
present: boolean;
ok: boolean;
status: "ok" | "failed" | "missing";
required: boolean;
exitCode: number | null;
version: string | null;
stdout: string | null;
stderr: string | null;
error: string | null;
}
export interface EnvDiagnostic {
name: string;
present: boolean;
ok: boolean;
required: boolean;
value: string | null;
status: "ok" | "missing" | "mismatch";
}
export interface FileDiagnostic {
path: string;
exists: boolean;
ok: boolean;
required: boolean;
kind: "file" | "directory" | "other" | "missing";
expectedKind: "file" | "directory" | "any" | null;
len: number | null;
}
export interface DiagnosticMessage {
level: "info" | "warning" | "error";
message: string;
}
export interface ToolingService {
/** Runs generic external-toolchain diagnostics for executables, env and files. */
diagnose(request: ToolchainDiagnosticRequest): Promise<ToolchainDiagnostic>;
}
export type PublicEventType = "workspaceFileChanged" | "backgroundTaskChanged";
export type PublicEvent = WorkspaceFileChangedEvent | BackgroundTaskChangedEvent;
export interface WorkspaceFileChangedEvent {
type: "workspaceFileChanged";
sequence: number;
occurredAtMs: number;
projectId: string;
path: string;
operation: string;
}
export interface BackgroundTaskChangedEvent {
type: "backgroundTaskChanged";
sequence: number;
occurredAtMs: number;
projectId: string;
taskId: string;
ownerAgentId: string;
state: string;
}
export interface EventSubscribeOptions {
/** Project to observe. Defaults to the focused project. */
projectId?: string;
/** Public event types to retain. Empty/omitted means every supported event. */
eventTypes?: PublicEventType[];
/** Per-subscription retained capacity. Host clamps to its supported bounds. */
capacity?: number;
/** Polling cadence used by the runtime facade. Defaults to 1000 ms. */
pollIntervalMs?: number;
/** Maximum events drained per poll. Host clamps to its supported bounds. */
maxEventsPerPoll?: number;
/** Called when the host reports dropped retained events for this subscription. */
onDropped?: (count: number) => void;
}
export interface EventSubscription {
readonly subscriptionId: string;
readonly projectId: string;
readonly eventTypes: PublicEventType[];
readonly retention: string;
dispose(): void;
}
export type EventHandler = (event: PublicEvent) => void;
export interface EventService {
/** Subscribes to stable, best-effort bounded public host/project events. */
subscribe(options: EventSubscribeOptions, handler: EventHandler): Promise<EventSubscription>;
}
export type JsonValue =
| null
| boolean
| number
| string
| JsonValue[]
| { [key: string]: JsonValue };
export type ConfigDocumentFormat = "json";
export type ConfigUpdateMode = "mergePatch" | "replace";
export interface ConfigDocumentReadOptions {
/** Project that owns the config document. Defaults to the focused project. */
projectId?: string;
/** Relative path under the project root. */
path: string;
/** Explicit format. Omit to infer from extension. First lot supports only `json`. */
format?: ConfigDocumentFormat;
}
export interface ConfigDocumentUpdateOptions extends ConfigDocumentReadOptions {
/** Update mode. Defaults host-side to `mergePatch`. */
mode?: ConfigUpdateMode;
/** Replacement value or JSON merge patch. */
value: JsonValue;
}
export interface ConfigDocument<T extends JsonValue = JsonValue> {
projectId: string;
path: string;
format: ConfigDocumentFormat;
value: T;
}
export interface ConfigDocumentWriteResult {
projectId: string;
path: string;
format: ConfigDocumentFormat;
mode: ConfigUpdateMode;
bytesWritten: number;
}
export interface ConfigDocumentService {
/** Reads and parses a structured config document. First lot supports JSON only. */
readDocument<T extends JsonValue = JsonValue>(
options: ConfigDocumentReadOptions,
): Promise<ConfigDocument<T>>;
/** Writes a full replacement or JSON merge patch. First lot supports JSON only. */
updateDocument(options: ConfigDocumentUpdateOptions): Promise<ConfigDocumentWriteResult>;
}
export interface BackgroundTaskService {
/**
* 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>;
/** Reads one command task directly from the host task store. */
getCommandStatus(taskId: string): Promise<CommandTaskStatus | null>;
/** 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>;
}
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>;
}