feat(sdk,plugins): API publique d'accès fichiers/workspace + analyse structure (#124,#129)
This commit is contained in:
@ -7,6 +7,12 @@ 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;
|
||||
- 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.
|
||||
|
||||
@ -70,6 +76,59 @@ export function activate(ctx: ActivateContext): void {
|
||||
}
|
||||
```
|
||||
|
||||
## Layout Runtime
|
||||
|
||||
Plugins can contribute custom layout panels by declaring `contributes.layouts`
|
||||
in `idea-plugin.json` and registering the matching layout type during
|
||||
`activate(ctx)`.
|
||||
|
||||
```json
|
||||
{
|
||||
"contributes": {
|
||||
"layouts": [
|
||||
{
|
||||
"type": "com.example.status",
|
||||
"label": "Status",
|
||||
"component": "StatusPanel"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
import type { ActivateContext, PluginLayoutProps } from "@idea/plugin-sdk";
|
||||
|
||||
function StatusPanel(props: PluginLayoutProps): string {
|
||||
return `status for ${props.projectId}`;
|
||||
}
|
||||
|
||||
export function activate(ctx: ActivateContext): void {
|
||||
const disposable = ctx.layouts?.register({
|
||||
type: "com.example.status",
|
||||
component: StatusPanel
|
||||
});
|
||||
if (disposable) ctx.subscriptions.push(disposable);
|
||||
}
|
||||
```
|
||||
|
||||
Public layout props are:
|
||||
|
||||
- `projectId`: project hosting the layout cell;
|
||||
- `nodeId`: stable layout node id for that cell instance;
|
||||
- `layoutType`: contributed layout type from the manifest;
|
||||
- `state`: opaque JSON-serializable state persisted by the host;
|
||||
- `setState(next)`: replaces that state;
|
||||
- `availability`: currently `"available"` when the component is mounted.
|
||||
|
||||
Lifecycle: register layouts during `activate(ctx)`, keep the returned disposable
|
||||
in `ctx.subscriptions`, and let the host dispose it on plugin unload. Layout
|
||||
components may be mounted, unmounted and remounted by the host; keep durable UI
|
||||
state in `state` via `setState`, not in module globals. Call `setState` from
|
||||
user actions, effects or asynchronous callbacks, not unconditionally while
|
||||
rendering. Services are available from `ctx.services` to plugins declaring the
|
||||
`tooling` capability; layout props do not expose private runtime gateways.
|
||||
|
||||
## Runtime Services
|
||||
|
||||
Plugins declaring the `tooling` capability receive `ctx.services`. Plugins
|
||||
@ -91,12 +150,215 @@ export async function activate(ctx: ActivateContext): Promise<void> {
|
||||
}
|
||||
```
|
||||
|
||||
### Workspace Files
|
||||
|
||||
Workspace paths are always relative to the project root. Hosts reject absolute
|
||||
paths, `..`, empty path segments and paths outside the sandbox. Text APIs use
|
||||
UTF-8; binary APIs use `Uint8Array`. Missing files reject on reads and resolve
|
||||
to `{ exists: false }` from `stat`.
|
||||
|
||||
```ts
|
||||
import type { ActivateContext } from "@idea/plugin-sdk";
|
||||
|
||||
export async function activate(ctx: ActivateContext): Promise<void> {
|
||||
const workspace = ctx.services?.workspace;
|
||||
const project = await workspace?.getCurrentProject();
|
||||
if (!workspace || !project) return;
|
||||
|
||||
await workspace.writeTextFile(".ideai/hello-plugin.txt", "hello\n", project.id);
|
||||
|
||||
const file = await workspace.readTextFile(".ideai/hello-plugin.txt", project.id);
|
||||
const listing = await workspace.listDirectory(".ideai", project.id);
|
||||
const stat = await workspace.stat(file.path, project.id);
|
||||
|
||||
ctx.logger.info("workspace file", {
|
||||
path: file.path,
|
||||
bytes: stat.len,
|
||||
entries: listing.entries.length
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`watch(path, handler, projectId?)` subscribes to public workspace file-change
|
||||
events for the given relative path. It is best-effort and bounded: plugins should
|
||||
handle missed events by refreshing their own derived state when needed.
|
||||
|
||||
### Project Structure
|
||||
|
||||
`queryStructure()` returns a bounded, generic read model so plugins do not each
|
||||
need to rescan the whole workspace for common markers:
|
||||
|
||||
```ts
|
||||
const structure = await ctx.services?.workspace.queryStructure({
|
||||
maxDepth: 3,
|
||||
maxEntries: 500
|
||||
});
|
||||
|
||||
for (const convention of structure?.conventions ?? []) {
|
||||
console.log(convention.id, convention.markerPath);
|
||||
}
|
||||
```
|
||||
|
||||
The MVP detects generic marker-file conventions such as `package.json`,
|
||||
`Cargo.toml`, `pyproject.toml`, `go.mod`, `Makefile` and `.git`. It deliberately
|
||||
does not expose language-specific ASTs or Android-specific concepts.
|
||||
|
||||
Current terminal scope is intentionally minimal: it opens or reattaches a shell
|
||||
PTY, writes bytes, resizes, detaches and closes. 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.
|
||||
PTY, writes bytes, resizes, detaches and closes.
|
||||
|
||||
### Command Tasks
|
||||
|
||||
Use `ctx.services.tasks.runCommand()` for non-interactive tools that should be
|
||||
tracked as IdeA background tasks instead of opening a raw PTY. `command` and
|
||||
`args` are passed separately, `cwd` is relative to the project root, and `env`
|
||||
adds process environment variables. The current host requires an `ownerAgentId`
|
||||
so the task can appear in Work and completion can be correlated to an agent.
|
||||
|
||||
```ts
|
||||
import type { ActivateContext } from "@idea/plugin-sdk";
|
||||
|
||||
export async function activate(ctx: ActivateContext): Promise<void> {
|
||||
const project = await ctx.services?.workspace.getCurrentProject();
|
||||
if (!project) return;
|
||||
|
||||
const task = await ctx.services?.tasks.runCommand({
|
||||
projectId: project.id,
|
||||
ownerAgentId: "00000000-0000-0000-0000-000000000000",
|
||||
label: "Check npm",
|
||||
command: "npm",
|
||||
args: ["--version"],
|
||||
cwd: ".",
|
||||
env: { CI: "1" },
|
||||
recordOnly: true
|
||||
});
|
||||
|
||||
const status = await ctx.services?.tasks.getCommandStatus(task.taskId);
|
||||
ctx.logger.info("command task", {
|
||||
taskId: task.taskId,
|
||||
state: status?.state,
|
||||
exitCode: status?.exitCode
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`list`, `getStatus`, `attachOutput`, `cancel` and `retry` continue to operate on
|
||||
tasks visible through IdeA's Work read model. `getCommandStatus` reads a launched
|
||||
command task directly from the host task store.
|
||||
|
||||
### Toolchain Diagnostics
|
||||
|
||||
Use `ctx.services.tooling.diagnose()` to check external prerequisites without
|
||||
hard-coding one stack into the SDK. A request can probe executables, inspect
|
||||
environment variables and validate workspace files in one structured result.
|
||||
|
||||
```ts
|
||||
import type { ActivateContext } from "@idea/plugin-sdk";
|
||||
|
||||
export async function activate(ctx: ActivateContext): Promise<void> {
|
||||
const diagnostic = await ctx.services?.tooling.diagnose({
|
||||
tools: [
|
||||
{
|
||||
id: "node",
|
||||
executable: "node",
|
||||
versionArgs: ["--version"],
|
||||
required: true
|
||||
}
|
||||
],
|
||||
env: [{ name: "PATH", required: true }],
|
||||
files: [{ path: "package.json", kind: "file" }]
|
||||
});
|
||||
|
||||
const node = diagnostic?.tools.find((tool) => tool.id === "node");
|
||||
ctx.logger.info("tooling diagnostic", {
|
||||
ok: diagnostic?.ok,
|
||||
nodePresent: node?.present,
|
||||
nodeVersion: node?.version,
|
||||
messages: diagnostic?.messages
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The diagnostic API is intentionally generic: it does not install tools, does not
|
||||
model Android devices or emulators, and does not expose language-specific ASTs.
|
||||
|
||||
### Events And Watch
|
||||
|
||||
Use `ctx.services.events.subscribe()` for stable public host/project events. The
|
||||
runtime hides the host polling details and returns a disposable subscription.
|
||||
|
||||
```ts
|
||||
import type { ActivateContext } from "@idea/plugin-sdk";
|
||||
|
||||
export async function activate(ctx: ActivateContext): Promise<void> {
|
||||
const subscription = await ctx.services?.events.subscribe(
|
||||
{
|
||||
eventTypes: ["backgroundTaskChanged"],
|
||||
capacity: 100,
|
||||
onDropped: (count) => ctx.logger.warn("plugin events dropped", { count })
|
||||
},
|
||||
(event) => {
|
||||
if (event.type === "backgroundTaskChanged") {
|
||||
ctx.logger.info("task changed", {
|
||||
taskId: event.taskId,
|
||||
state: event.state
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (subscription) ctx.subscriptions.push(subscription);
|
||||
|
||||
const watch = await ctx.services?.workspace.watch("src", (event) => {
|
||||
ctx.logger.info("workspace changed", {
|
||||
path: event.path,
|
||||
kind: event.kind,
|
||||
operation: event.operation
|
||||
});
|
||||
});
|
||||
|
||||
if (watch) ctx.subscriptions.push(watch);
|
||||
}
|
||||
```
|
||||
|
||||
Public event retention is `bestEffortBounded`: events are retained per
|
||||
subscription up to the requested/host-capped capacity, drained oldest-first, and
|
||||
`onDropped` reports when older retained events were overwritten.
|
||||
|
||||
### Structured Config Documents
|
||||
|
||||
Use `ctx.services.config` when a plugin needs to read or update a structured
|
||||
configuration file without reimplementing parsing and serialization.
|
||||
|
||||
First-lot format support is deliberately narrow:
|
||||
|
||||
- `json` only;
|
||||
- inferred from `.json` when `format` is omitted;
|
||||
- serialized as pretty JSON with a trailing newline;
|
||||
- update modes: `mergePatch` and `replace`;
|
||||
- `mergePatch` follows JSON merge-patch semantics: object keys are merged
|
||||
recursively and `null` removes a key.
|
||||
|
||||
```ts
|
||||
import type { ActivateContext } from "@idea/plugin-sdk";
|
||||
|
||||
export async function activate(ctx: ActivateContext): Promise<void> {
|
||||
const config = await ctx.services?.config.readDocument({
|
||||
path: ".ideai/hello-plugin.json"
|
||||
});
|
||||
|
||||
await ctx.services?.config.updateDocument({
|
||||
path: ".ideai/hello-plugin.json",
|
||||
mode: "mergePatch",
|
||||
value: {
|
||||
enabled: true,
|
||||
lastReadFormat: config?.format ?? "json"
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
YAML, TOML, XML, `.properties` and stack-specific config models are not part of
|
||||
this first lot.
|
||||
|
||||
Declare the additive `tooling` capability to receive `ctx.services` at runtime:
|
||||
|
||||
|
||||
@ -1,29 +1,11 @@
|
||||
import type { ActivateContext, CommandDisposable, IdeAPluginModule } from "@idea/plugin-sdk";
|
||||
import type { ActivateContext, IdeAPluginModule, PluginLayoutProps } from "@idea/plugin-sdk";
|
||||
|
||||
const COMMAND_ID = "hello-plugin";
|
||||
const LAYOUT_TYPE = "hello-plugin.hello-world";
|
||||
|
||||
type HelloPluginLayoutProps = {
|
||||
projectId?: string;
|
||||
nodeId?: string;
|
||||
layoutType?: string;
|
||||
state?: unknown;
|
||||
};
|
||||
|
||||
type LayoutRegistry = {
|
||||
register(definition: {
|
||||
type: string;
|
||||
component: (props: HelloPluginLayoutProps) => string;
|
||||
}): CommandDisposable;
|
||||
};
|
||||
|
||||
type HelloPluginContext = ActivateContext & {
|
||||
layouts?: LayoutRegistry;
|
||||
};
|
||||
|
||||
let hasLoggedFirstLayoutRender = false;
|
||||
|
||||
function HelloWorldLayout(props: HelloPluginLayoutProps): string {
|
||||
function HelloWorldLayout(props: PluginLayoutProps): string {
|
||||
if (!hasLoggedFirstLayoutRender) {
|
||||
hasLoggedFirstLayoutRender = true;
|
||||
console.info("[hello-plugin] layout first render", {
|
||||
@ -38,14 +20,13 @@ function HelloWorldLayout(props: HelloPluginLayoutProps): string {
|
||||
}
|
||||
|
||||
export function activate(ctx: ActivateContext): void {
|
||||
const pluginContext = ctx as HelloPluginContext;
|
||||
ctx.logger.info("activating hello-plugin", {
|
||||
pluginId: ctx.pluginId,
|
||||
hasCommands: Boolean(pluginContext.commands),
|
||||
hasLayouts: Boolean(pluginContext.layouts)
|
||||
hasCommands: Boolean(ctx.commands),
|
||||
hasLayouts: Boolean(ctx.layouts)
|
||||
});
|
||||
|
||||
const commandDisposable = pluginContext.commands?.registerCommand(COMMAND_ID, () => {
|
||||
const commandDisposable = ctx.commands?.registerCommand(COMMAND_ID, () => {
|
||||
ctx.logger.info("command executed", { commandId: COMMAND_ID });
|
||||
return "hello-world";
|
||||
});
|
||||
@ -57,7 +38,7 @@ export function activate(ctx: ActivateContext): void {
|
||||
ctx.logger.warn("command registry unavailable", { commandId: COMMAND_ID });
|
||||
}
|
||||
|
||||
const layoutDisposable = pluginContext.layouts?.register({
|
||||
const layoutDisposable = ctx.layouts?.register({
|
||||
type: LAYOUT_TYPE,
|
||||
component: HelloWorldLayout
|
||||
});
|
||||
@ -72,12 +53,130 @@ export function activate(ctx: ActivateContext): void {
|
||||
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)
|
||||
});
|
||||
void useWorkspaceSdk(ctx);
|
||||
}
|
||||
|
||||
async function useWorkspaceSdk(ctx: ActivateContext): Promise<void> {
|
||||
const workspace = ctx.services?.workspace;
|
||||
if (!workspace) return;
|
||||
|
||||
const project = await workspace.getCurrentProject();
|
||||
if (!project) {
|
||||
ctx.logger.info("workspace service available without a focused project");
|
||||
return;
|
||||
}
|
||||
|
||||
const fixturePath = ".ideai/hello-plugin.txt";
|
||||
await workspace.writeTextFile(fixturePath, "hello from @idea/plugin-sdk\n", project.id);
|
||||
const file = await workspace.readTextFile(fixturePath, project.id);
|
||||
const stat = await workspace.stat(fixturePath, project.id);
|
||||
const listing = await workspace.listDirectory(".ideai", project.id);
|
||||
const structure = await workspace.queryStructure({
|
||||
projectId: project.id,
|
||||
maxDepth: 2,
|
||||
maxEntries: 100
|
||||
});
|
||||
|
||||
ctx.logger.info("workspace file round-trip complete", {
|
||||
projectId: project.id,
|
||||
path: file.path,
|
||||
bytes: stat.len,
|
||||
ideaiEntries: listing.entries.length,
|
||||
conventions: structure.conventions.map((convention) => convention.id)
|
||||
});
|
||||
|
||||
const diagnostic = await ctx.services?.tooling.diagnose({
|
||||
projectId: project.id,
|
||||
tools: [
|
||||
{
|
||||
id: "echo",
|
||||
executable: "echo",
|
||||
versionArgs: ["hello-plugin-toolcheck"],
|
||||
required: true
|
||||
}
|
||||
],
|
||||
env: [{ name: "PATH", required: true }],
|
||||
files: [{ path: fixturePath, kind: "file" }]
|
||||
});
|
||||
|
||||
ctx.logger.info("tooling diagnostic complete", {
|
||||
ok: diagnostic?.ok,
|
||||
echoVersion: diagnostic?.tools.find((tool) => tool.id === "echo")?.version,
|
||||
messages: diagnostic?.messages
|
||||
});
|
||||
|
||||
const configPath = ".ideai/hello-plugin.json";
|
||||
await workspace.writeTextFile(
|
||||
configPath,
|
||||
JSON.stringify({ enabled: true, launches: 0 }, null, 2) + "\n",
|
||||
project.id
|
||||
);
|
||||
const configDocument = await ctx.services?.config.readDocument({
|
||||
projectId: project.id,
|
||||
path: configPath
|
||||
});
|
||||
await ctx.services?.config.updateDocument({
|
||||
projectId: project.id,
|
||||
path: configPath,
|
||||
mode: "mergePatch",
|
||||
value: { lastFormat: configDocument?.format ?? "json", launches: 1 }
|
||||
});
|
||||
ctx.logger.info("config document updated", {
|
||||
path: configDocument?.path,
|
||||
format: configDocument?.format
|
||||
});
|
||||
|
||||
const watch = await workspace.watch(".ideai", (event) => {
|
||||
ctx.logger.info("workspace watch event", {
|
||||
path: event.path,
|
||||
kind: event.kind,
|
||||
operation: event.operation
|
||||
});
|
||||
}, project.id);
|
||||
ctx.subscriptions.push(watch);
|
||||
|
||||
const events = await ctx.services?.events.subscribe(
|
||||
{
|
||||
projectId: project.id,
|
||||
eventTypes: ["backgroundTaskChanged"],
|
||||
pollIntervalMs: 2000,
|
||||
onDropped: (count) => ctx.logger.warn("plugin events dropped", { count })
|
||||
},
|
||||
(event) => {
|
||||
if (event.type === "backgroundTaskChanged") {
|
||||
ctx.logger.info("background task changed", {
|
||||
taskId: event.taskId,
|
||||
state: event.state
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
if (events) ctx.subscriptions.push(events);
|
||||
|
||||
const ownerAgentId = await ctx.storage?.get<string>("helloPlugin.ownerAgentId");
|
||||
if (!ownerAgentId) {
|
||||
ctx.logger.info("command task example skipped: no owner agent configured");
|
||||
return;
|
||||
}
|
||||
|
||||
const task = await ctx.services?.tasks.runCommand({
|
||||
projectId: project.id,
|
||||
ownerAgentId,
|
||||
label: "Hello plugin command",
|
||||
command: "echo",
|
||||
args: ["hello from @idea/plugin-sdk"],
|
||||
cwd: ".",
|
||||
recordOnly: true
|
||||
});
|
||||
|
||||
if (task) {
|
||||
const status = await ctx.services?.tasks.getCommandStatus(task.taskId);
|
||||
ctx.logger.info("command task launched", {
|
||||
taskId: task.taskId,
|
||||
state: status?.state ?? task.state,
|
||||
exitCode: status?.exitCode ?? task.exitCode
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const plugin: IdeAPluginModule = {
|
||||
|
||||
@ -14,22 +14,72 @@ export {
|
||||
} from "./manifest.js";
|
||||
export type {
|
||||
ActivateContext,
|
||||
BackgroundTaskChangedEvent,
|
||||
CommandDisposable,
|
||||
CommandHandler,
|
||||
CommandRegistry,
|
||||
CommandTaskStatus,
|
||||
ConfigDocument,
|
||||
ConfigDocumentFormat,
|
||||
ConfigDocumentReadOptions,
|
||||
ConfigDocumentService,
|
||||
ConfigDocumentUpdateOptions,
|
||||
ConfigDocumentWriteResult,
|
||||
ConfigUpdateMode,
|
||||
DiagnosticMessage,
|
||||
EnvDiagnostic,
|
||||
EnvRequirement,
|
||||
EventHandler,
|
||||
EventService,
|
||||
EventSubscribeOptions,
|
||||
EventSubscription,
|
||||
FileDiagnostic,
|
||||
FileRequirement,
|
||||
BackgroundTaskOutputAttachment,
|
||||
BackgroundTaskRetryResult,
|
||||
BackgroundTaskService,
|
||||
BackgroundTaskStatus,
|
||||
IdeAPluginModule,
|
||||
JsonValue,
|
||||
LayoutRegistry,
|
||||
PluginLogger,
|
||||
PluginLayoutAvailability,
|
||||
PluginLayoutComponent,
|
||||
PluginLayoutDefinition,
|
||||
PluginLayoutProps,
|
||||
PluginLayoutRenderResult,
|
||||
PluginLayoutState,
|
||||
PluginServices,
|
||||
PluginStorage,
|
||||
ProjectConvention,
|
||||
ProjectModule,
|
||||
ProjectStructure,
|
||||
ProjectStructureEntry,
|
||||
ProjectStructureEntryKind,
|
||||
PublicEvent,
|
||||
PublicEventType,
|
||||
RunCommandTaskOptions,
|
||||
TerminalOpenOptions,
|
||||
TerminalReattachOptions,
|
||||
TerminalReattachResult,
|
||||
TerminalService,
|
||||
TerminalSession,
|
||||
ToolchainDiagnostic,
|
||||
ToolchainDiagnosticRequest,
|
||||
ToolDiagnostic,
|
||||
ToolingService,
|
||||
ToolRequirement,
|
||||
WorkspaceBinaryFile,
|
||||
WorkspaceDirEntry,
|
||||
WorkspaceDirectoryListing,
|
||||
WorkspaceFileChangedEvent,
|
||||
WorkspaceProject,
|
||||
WorkspaceService
|
||||
WorkspaceResolvedPath,
|
||||
WorkspaceService,
|
||||
WorkspaceStat,
|
||||
WorkspaceStructureQuery,
|
||||
WorkspaceTextFile,
|
||||
WorkspaceWatch,
|
||||
WorkspaceWatchEvent,
|
||||
WorkspaceWatchHandler
|
||||
} from "./runtime.js";
|
||||
|
||||
@ -3,6 +3,7 @@ export interface ActivateContext {
|
||||
logger: PluginLogger;
|
||||
subscriptions: CommandDisposable[];
|
||||
commands?: CommandRegistry;
|
||||
layouts?: LayoutRegistry;
|
||||
storage?: PluginStorage;
|
||||
/**
|
||||
* Stable public service facade for plugins that need workspace, background
|
||||
@ -40,9 +41,47 @@ export interface PluginStorage {
|
||||
delete(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
export type PluginLayoutState = JsonValue | undefined;
|
||||
export type PluginLayoutAvailability = "available";
|
||||
export type PluginLayoutRenderResult = unknown;
|
||||
|
||||
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> = (
|
||||
props: PluginLayoutProps<TState>,
|
||||
) => PluginLayoutRenderResult;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -61,6 +100,117 @@ export interface WorkspaceService {
|
||||
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.
|
||||
*/
|
||||
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 {
|
||||
@ -88,7 +238,247 @@ export interface BackgroundTaskRetryResult {
|
||||
taskId?: string;
|
||||
}
|
||||
|
||||
export interface RunCommandTaskOptions {
|
||||
/** Project that owns the command workspace. Defaults to the focused project. */
|
||||
projectId?: string;
|
||||
/** Agent id used by IdeA Work for ownership, cancellation and completion delivery. */
|
||||
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. */
|
||||
recordOnly?: boolean;
|
||||
/** Optional absolute deadline, epoch milliseconds. */
|
||||
deadlineMs?: number;
|
||||
}
|
||||
|
||||
export interface CommandTaskStatus {
|
||||
taskId: string;
|
||||
ownerAgentId: string;
|
||||
projectId: string;
|
||||
kind: string;
|
||||
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. */
|
||||
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. */
|
||||
|
||||
Reference in New Issue
Block a user