feat(background-task): rendu live des tâches de fond — subscriber UI + canal IPC (#58)
Câble un canal attachable au flux de sortie d'une tâche de fond (runner infrastructure + commande app-tauri + port/adaptateurs frontend) et le panneau ProjectWorkStatePanel s'y abonne pour un rendu live au lieu d'un état figé au dernier snapshot. Validations obtenues avant commit : - cargo test -p infrastructure --test background_task_runner : vert - cargo check -p backend -p app-tauri : vert - npx vitest run src/features/workstate/workstate.test.tsx : vert - npm run typecheck : vert - verdict QA #58 : vert Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -124,6 +124,7 @@ import type {
|
||||
FocusedProject,
|
||||
FocusedProjectGateway,
|
||||
WorkStateGateway,
|
||||
BackgroundTaskAttachment,
|
||||
} from "@/ports";
|
||||
import { normalizeProjectWorkState } from "../workStateNormalization";
|
||||
import { applyOperation, singleLeafTree } from "@/features/layout/layout";
|
||||
@ -2519,18 +2520,76 @@ export class MockPermissionGateway implements PermissionGateway {
|
||||
|
||||
export class MockWorkStateGateway implements WorkStateGateway {
|
||||
private states = new Map<string, ProjectWorkState>();
|
||||
private backgroundTaskStreams = new Map<
|
||||
string,
|
||||
{
|
||||
scrollback: Uint8Array;
|
||||
live: boolean;
|
||||
sinks: Set<(bytes: Uint8Array) => void>;
|
||||
}
|
||||
>();
|
||||
|
||||
/** Seeds the read-model returned for a project (deterministic tests/dev). */
|
||||
_setProjectWorkState(projectId: string, state: unknown): void {
|
||||
this.states.set(projectId, normalizeProjectWorkState(state));
|
||||
}
|
||||
|
||||
/** Seeds attach output for a background task (deterministic tests/dev). */
|
||||
_setBackgroundTaskAttachment(
|
||||
taskId: string,
|
||||
attachment: { scrollback?: string | Uint8Array; live?: boolean },
|
||||
): void {
|
||||
const encoder = new TextEncoder();
|
||||
const scrollback =
|
||||
typeof attachment.scrollback === "string"
|
||||
? encoder.encode(attachment.scrollback)
|
||||
: attachment.scrollback ?? new Uint8Array();
|
||||
this.backgroundTaskStreams.set(taskId, {
|
||||
scrollback,
|
||||
live: attachment.live ?? false,
|
||||
sinks: new Set(),
|
||||
});
|
||||
}
|
||||
|
||||
/** Emits a live output chunk to current subscribers (deterministic tests/dev). */
|
||||
_emitBackgroundTaskOutput(taskId: string, bytes: string | Uint8Array): void {
|
||||
const stream = this.backgroundTaskStreams.get(taskId);
|
||||
if (!stream) return;
|
||||
const encoder = new TextEncoder();
|
||||
const chunk = typeof bytes === "string" ? encoder.encode(bytes) : bytes;
|
||||
for (const sink of stream.sinks) sink(chunk);
|
||||
}
|
||||
|
||||
_backgroundTaskSubscriberCount(taskId: string): number {
|
||||
return this.backgroundTaskStreams.get(taskId)?.sinks.size ?? 0;
|
||||
}
|
||||
|
||||
async getProjectWorkState(projectId: string): Promise<ProjectWorkState> {
|
||||
return structuredClone(
|
||||
this.states.get(projectId) ?? { agents: [], conversations: [] },
|
||||
);
|
||||
}
|
||||
|
||||
async attachBackgroundTask(
|
||||
taskId: string,
|
||||
onData: (bytes: Uint8Array) => void,
|
||||
): Promise<BackgroundTaskAttachment> {
|
||||
const stream = this.backgroundTaskStreams.get(taskId) ?? {
|
||||
scrollback: new Uint8Array(),
|
||||
live: false,
|
||||
sinks: new Set<(bytes: Uint8Array) => void>(),
|
||||
};
|
||||
if (stream.live) stream.sinks.add(onData);
|
||||
return {
|
||||
taskId,
|
||||
scrollback: Uint8Array.from(stream.scrollback),
|
||||
live: stream.live,
|
||||
detach: () => {
|
||||
stream.sinks.delete(onData);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async cancelBackgroundTask(_taskId: string): Promise<void> {
|
||||
// No-op in the mock; real refresh is driven by domain events.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user