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:
217
frontend/src/plugins/runtime/services.ts
Normal file
217
frontend/src/plugins/runtime/services.ts
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user