feat(backend,frontend): lot utilisateur #346 #347 #348 — exécution workspace plugin contrôlée par l'hôte, réordonnancement des onglets projet par glisser-déposer, Final restauré après stop d'agent CLI custom (QA verte)
#346 — capacités plugin SCM: le manifeste permissions.workspaceExec (contrat SDK 8ff0f47) est appliqué côté hôte — domain PluginWorkspaceExec Rule/Effect + audit sink, port WorkspaceToolRunner, adapter LocalWorkspaceToolRunner sandboxé (concurrence 4, timeout, bornes stdout/stderr, résolution bare executable sans shell), service application + DTO backend + commandes app-tauri, gateway frontend execTool exposée au runtime plugin — fin des lectures ad hoc du store interne .git par les plugins. #347 — onglets projet réordonnables par glisser-déposer (Tabs DnD, onReorder, persistance de l'ordre via useProjects). #348 — stop d'agent CLI custom: le message Final vert réapparaît au tour suivant (override terminal stop armé correctement, reset de l'interaction, refresh de la délégation requester). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -64,6 +64,8 @@ export {
|
||||
type WorkspaceBinaryFile,
|
||||
type WorkspaceDirEntry,
|
||||
type WorkspaceDirectoryListing,
|
||||
type WorkspaceExecToolRequest,
|
||||
type WorkspaceExecToolResult,
|
||||
type WorkspaceResolvedPath,
|
||||
type WorkspaceStat,
|
||||
type WorkspaceStructureQuery,
|
||||
|
||||
@ -268,6 +268,7 @@ describe("loadPlugins", () => {
|
||||
"workspace",
|
||||
]);
|
||||
expect((globalThis as Record<string, unknown>).__workspaceServiceKeys).toEqual([
|
||||
"execTool",
|
||||
"getCurrentProject",
|
||||
"getProjectRoot",
|
||||
"listDirectory",
|
||||
|
||||
@ -125,6 +125,16 @@ function gateways(overrides: {
|
||||
...overrides.window,
|
||||
};
|
||||
const pluginWorkspace: PluginWorkspaceGateway = {
|
||||
execTool: vi.fn(async () => ({
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
timedOut: false,
|
||||
stdout: "ok\n",
|
||||
stderr: "",
|
||||
stdoutTruncated: false,
|
||||
stderrTruncated: false,
|
||||
durationMs: 4,
|
||||
})),
|
||||
readText: vi.fn(async ({ path }) => ({ path, content: "file text" })),
|
||||
readBinary: vi.fn(async ({ path }) => ({ path, bytes: new Uint8Array([67]) })),
|
||||
writeText: vi.fn(),
|
||||
@ -332,6 +342,21 @@ describe("createPluginServices", () => {
|
||||
expect(g.project.updateProjectContext).toHaveBeenCalledWith("project-1", "next context");
|
||||
});
|
||||
|
||||
it("injects plugin and project identity into workspace execTool", async () => {
|
||||
const g = gateways();
|
||||
const services = createPluginServices(g, { pluginId: "dev.acme.plugin" });
|
||||
|
||||
await expect(
|
||||
services.workspace.execTool({ command: "tool", args: ["status"] }),
|
||||
).resolves.toMatchObject({ exitCode: 0, stdout: "ok\n", timedOut: false });
|
||||
expect(g.pluginWorkspace.execTool).toHaveBeenCalledWith(expect.objectContaining({
|
||||
pluginId: "dev.acme.plugin",
|
||||
projectId: "project-1",
|
||||
command: "tool",
|
||||
args: ["status"],
|
||||
}));
|
||||
});
|
||||
|
||||
it("maps background tasks through the work-state gateway and wraps output attachment", async () => {
|
||||
const detach = vi.fn();
|
||||
const onData = vi.fn();
|
||||
|
||||
@ -13,6 +13,7 @@ import type {
|
||||
PluginConfigGateway,
|
||||
PluginEventGateway,
|
||||
PluginProjectStructureQuery,
|
||||
PluginWorkspaceExecInput as HostPluginWorkspaceExecInput,
|
||||
PluginTaskGateway,
|
||||
PluginToolchainGateway,
|
||||
PluginWorkspaceGateway,
|
||||
@ -52,6 +53,31 @@ export interface WorkspaceService {
|
||||
stat(path: string, projectId?: string): Promise<WorkspaceStat>;
|
||||
watch(path: string, handler: WorkspaceWatchHandler, projectId?: string): Promise<WorkspaceWatch>;
|
||||
queryStructure(query?: WorkspaceStructureQuery): Promise<ProjectStructure>;
|
||||
/** Runs one manifest-allowlisted binary directly, without a visible task. */
|
||||
execTool(request: WorkspaceExecToolRequest): Promise<WorkspaceExecToolResult>;
|
||||
}
|
||||
|
||||
export interface WorkspaceExecToolRequest {
|
||||
projectId?: string;
|
||||
command: string;
|
||||
args?: string[];
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
timeoutMs?: number;
|
||||
maxStdoutBytes?: number;
|
||||
maxStderrBytes?: number;
|
||||
stdin?: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceExecToolResult {
|
||||
exitCode: number | null;
|
||||
signal?: string;
|
||||
timedOut: boolean;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
stdoutTruncated: boolean;
|
||||
stderrTruncated: boolean;
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
export interface WorkspaceResolvedPath {
|
||||
@ -651,6 +677,33 @@ export function createPluginServices(
|
||||
};
|
||||
return gateways.pluginWorkspace.queryProjectStructure(input);
|
||||
},
|
||||
async execTool(request) {
|
||||
if (!scope.pluginId) throw new Error("pluginId is required for workspace.execTool");
|
||||
const project = await requireProject(request.projectId);
|
||||
const input: HostPluginWorkspaceExecInput = {
|
||||
pluginId: scope.pluginId,
|
||||
projectId: project.id,
|
||||
command: request.command,
|
||||
args: request.args,
|
||||
cwd: request.cwd,
|
||||
env: request.env,
|
||||
timeoutMs: request.timeoutMs,
|
||||
maxStdoutBytes: request.maxStdoutBytes,
|
||||
maxStderrBytes: request.maxStderrBytes,
|
||||
stdin: request.stdin,
|
||||
};
|
||||
const result = await gateways.pluginWorkspace.execTool(input);
|
||||
return {
|
||||
exitCode: result.exitCode,
|
||||
...(result.signal ? { signal: result.signal } : {}),
|
||||
timedOut: result.timedOut,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
stdoutTruncated: result.stdoutTruncated,
|
||||
stderrTruncated: result.stderrTruncated,
|
||||
durationMs: result.durationMs,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
async function listTasks(projectId?: string): Promise<BackgroundTaskStatus[]> {
|
||||
|
||||
Reference in New Issue
Block a user