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:
2026-09-13 12:23:44 +02:00
parent 2b0d82efa3
commit 561c50f6ed
28 changed files with 1761 additions and 51 deletions

View File

@ -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[]> {