Files
IdeA/frontend/src/features/workstate/useProjectWorkState.ts
Blomios 9ee06c7ec9 fix(domain,application,infrastructure,backend,web-server,app-tauri,frontend): statut/LED projet indexé par projectId, source LiveSessions/LiveStateStore/ReconcileLiveState — la fin d'un agent dans un projet non actif repasse désormais la LED du projet au repos — domain: DomainEvent::AgentBusyChanged porte project_id ; infrastructure input: busy émis au démarrage du tour et idle au mark_idle avec le projet possesseur ; backend: DTO agentBusyChanged sérialise project_id (busy_changed_keeps_owning_project_on_the_wire) ; application workstate: ReconcileLiveState exécuté aussi avant les lectures de projection (plus seulement à l'ouverture) et liveness évaluée par (projectId, agentId) — un même agent_id vivant dans un autre projet n'épargne plus un orphelin ; web-server: ws_send_chat libère le busy avant le Final visible et la route work-state projet reste au contrat tauri ; app-tauri: commandes work-state/busy alignées sur le projectId possesseur ; frontend: useProjectActivity agrège l'activité par projectId (busy_changed filtré par projet possesseur, plus le seul projet courant), wsLiveClient/wsLiveClientReconnect propagant project_id, AnnouncementsProvider/CustomAgentChatView/WebWorkspaceLive/LayoutGrid.cellControlsLayering alignés — #373 (QA verte: cargo check --tests -p domain -p application -p infrastructure -p backend -p app-tauri -p web-server, cargo test -p application workstate::reconcile::tests, cargo test -p infrastructure input::tests::busy_event_fires_on_turn_start_and_idle_on_mark_idle, cargo test -p backend events::tests::busy_changed_keeps_owning_project_on_the_wire, cargo test -p web-server websocket_chat_send_releases_busy_before_visible_final + authorized_get_project_work_state_returns_tauri_contract, npm test -- useProjectActivity wsLiveClient wsLiveClientReconnect workstate AnnouncementsProvider CustomAgentChatView WebWorkspaceLive LayoutGrid.cellControlsLayering, npm run typecheck, cargo fmt --all -- --check, git diff --check ; fichiers .ideai/* et scripts/ hors périmètre ; aucun push)
2026-09-17 20:23:36 +02:00

90 lines
2.4 KiB
TypeScript

/**
* 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";
import { onAgentRosterChanged } from "@/features/agents/agentRosterEvents";
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(
() =>
onAgentRosterChanged((change) => {
if (change.projectId === projectId) void refresh();
}),
[projectId, refresh],
);
useEffect(() => {
if (!system) return;
let unsubscribe: (() => void) | undefined;
let cancelled = false;
void system.onDomainEvent((event) => {
if (event.type === "agentBusyChanged" && event.projectId !== projectId) {
return;
}
if (
event.type === "agentLaunched" ||
event.type === "agentExited" ||
event.type === "agentBusyChanged" ||
event.type === "delegationReady" ||
event.type === "orchestratorRequestProcessed" ||
event.type === "backgroundTaskChanged" ||
event.type === "agentInboxChanged" ||
event.type === "agentWakeChanged"
) {
void refresh();
}
}).then((u) => {
if (cancelled) u();
else unsubscribe = u;
});
return () => {
cancelled = true;
unsubscribe?.();
};
}, [refresh, system]);
return { state, error, busy, refresh };
}