feat(workstate): UI live-state des conversations/délégations (Lot A frontend)
Ajoute le port et l'adaptateur workState (+ mock) côté frontend, le type domaine associé, et la feature `workstate` (panneau ProjectWorkStatePanel + hook useProjectWorkState) consommant le read-model live exposé par le backend. Intègre le panneau dans ProjectsView. Tests verts (workstate + projects). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
73
frontend/src/features/workstate/useProjectWorkState.ts
Normal file
73
frontend/src/features/workstate/useProjectWorkState.ts
Normal file
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* View-model hook for the project work-state read-model.
|
||||
*
|
||||
* It consumes the WorkStateGateway only, and optionally subscribes to existing
|
||||
* domain events through SystemGateway to refresh the read-only snapshot.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import type { GatewayError, ProjectWorkState } from "@/domain";
|
||||
import { useGateways } from "@/app/di";
|
||||
|
||||
export interface ProjectWorkStateViewModel {
|
||||
state: ProjectWorkState | null;
|
||||
error: string | null;
|
||||
busy: boolean;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
function describe(e: unknown): string {
|
||||
if (e && typeof e === "object" && "message" in e) {
|
||||
return String((e as GatewayError).message);
|
||||
}
|
||||
return String(e);
|
||||
}
|
||||
|
||||
export function useProjectWorkState(projectId: string): ProjectWorkStateViewModel {
|
||||
const { workState, system } = useGateways();
|
||||
const [state, setState] = useState<ProjectWorkState | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
setState(await workState.getProjectWorkState(projectId));
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [projectId, workState]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!system) return;
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
let cancelled = false;
|
||||
void system.onDomainEvent((event) => {
|
||||
if (
|
||||
event.type === "agentLaunched" ||
|
||||
event.type === "agentExited" ||
|
||||
event.type === "agentBusyChanged" ||
|
||||
event.type === "orchestratorRequestProcessed"
|
||||
) {
|
||||
void refresh();
|
||||
}
|
||||
}).then((u) => {
|
||||
if (cancelled) u();
|
||||
else unsubscribe = u;
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, [refresh, system]);
|
||||
|
||||
return { state, error, busy, refresh };
|
||||
}
|
||||
Reference in New Issue
Block a user