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

@ -0,0 +1,217 @@
import type { BackgroundCompletion } from "@/domain";
import type {
BackgroundTaskAttachment,
FocusedProject,
FocusedProjectGateway,
OpenTerminalOptions,
ProjectGateway,
TerminalGateway,
WorkStateGateway,
} from "@/ports";
export interface PluginServices {
workspace: WorkspaceService;
tasks: BackgroundTaskService;
terminal: TerminalService;
}
export interface WorkspaceProject {
id: string;
name: string;
root: string;
}
export interface WorkspaceService {
getCurrentProject(): Promise<WorkspaceProject | null>;
getProjectRoot(projectId?: string): Promise<string>;
readProjectContext(projectId?: string): Promise<string>;
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 {
taskId?: string;
}
export interface BackgroundTaskService {
list(projectId?: string): Promise<BackgroundTaskStatus[]>;
getStatus(taskId: string, projectId?: string): Promise<BackgroundTaskStatus | null>;
attachOutput(
taskId: string,
onData: (bytes: Uint8Array) => void,
): Promise<BackgroundTaskOutputAttachment>;
cancel(taskId: string): Promise<void>;
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 {
open(options?: TerminalOpenOptions): Promise<TerminalSession>;
reattach(sessionId: string, options?: TerminalReattachOptions): Promise<TerminalReattachResult>;
close(sessionId: string): Promise<void>;
}
interface PluginServiceGatewaySet {
project: ProjectGateway;
terminal: TerminalGateway;
workState: WorkStateGateway;
focusedProject: FocusedProjectGateway;
}
const DEFAULT_ROWS = 24;
const DEFAULT_COLS = 80;
function noopDataHandler(): void {
// Intentionally empty: plugin code may opt into output bytes per call.
}
function toWorkspaceProject(project: FocusedProject): WorkspaceProject {
return { id: project.id, name: project.name, root: project.root };
}
function toBackgroundTaskStatus(task: BackgroundCompletion): BackgroundTaskStatus {
return {
taskId: task.taskId,
ownerAgentId: task.ownerAgentId,
projectId: task.projectId,
kind: task.kind,
status: task.status,
exitCode: task.exitCode,
summary: task.summary,
stdoutTail: task.stdoutTail,
stderrTail: task.stderrTail,
updatedAtMs: task.updatedAtMs,
};
}
export function createPluginServices(gateways: PluginServiceGatewaySet): PluginServices {
async function currentProject(): Promise<WorkspaceProject | null> {
const focused = await gateways.focusedProject.getFocusedProject();
return focused ? toWorkspaceProject(focused) : null;
}
async function requireProject(projectId?: string): Promise<WorkspaceProject> {
if (projectId) {
const project = (await gateways.project.listProjects()).find((p) => p.id === projectId);
if (!project) throw new Error(`project not found: ${projectId}`);
return { id: project.id, name: project.name, root: project.root };
}
const focused = await currentProject();
if (!focused) throw new Error("no current project is focused");
return focused;
}
const workspace: WorkspaceService = {
getCurrentProject: currentProject,
async getProjectRoot(projectId) {
return (await requireProject(projectId)).root;
},
async readProjectContext(projectId) {
const project = await requireProject(projectId);
return gateways.project.readProjectContext(project.id);
},
async updateProjectContext(content, projectId) {
const project = await requireProject(projectId);
await gateways.project.updateProjectContext(project.id, content);
},
};
async function listTasks(projectId?: string): Promise<BackgroundTaskStatus[]> {
const project = await requireProject(projectId);
const state = await gateways.workState.getProjectWorkState(project.id);
return state.agents
.flatMap((agent) => agent.backgroundTasks ?? [])
.map(toBackgroundTaskStatus);
}
const tasks: BackgroundTaskService = {
list: listTasks,
async getStatus(taskId, projectId) {
return (await listTasks(projectId)).find((task) => task.taskId === taskId) ?? null;
},
async attachOutput(taskId, onData) {
const attachment: BackgroundTaskAttachment =
await gateways.workState.attachBackgroundTask(taskId, onData);
return {
taskId: attachment.taskId,
scrollback: attachment.scrollback,
live: attachment.live,
detach: () => attachment.detach(),
};
},
async cancel(taskId) {
await gateways.workState.cancelBackgroundTask(taskId);
},
async retry(taskId) {
await gateways.workState.retryBackgroundTask(taskId);
return {};
},
};
const terminal: TerminalService = {
async open(options = {}) {
const cwd = options.cwd ?? (await workspace.getProjectRoot());
const openOptions: OpenTerminalOptions = {
cwd,
rows: options.rows ?? DEFAULT_ROWS,
cols: options.cols ?? DEFAULT_COLS,
};
return gateways.terminal.openTerminal(openOptions, options.onData ?? noopDataHandler);
},
async reattach(sessionId, options = {}) {
const result = await gateways.terminal.reattach(
sessionId,
options.onData ?? noopDataHandler,
);
return { session: result.handle, scrollback: result.scrollback };
},
async close(sessionId) {
await gateways.terminal.closeTerminal(sessionId);
},
};
return { workspace, tasks, terminal };
}