Files
IdeA/frontend/src/features/projects/projectCliInteractionActivity.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

86 lines
2.2 KiB
TypeScript

import { useEffect, useState } from "react";
type Listener = () => void;
const activeByProject = new Map<string, Set<string>>();
const listeners = new Set<Listener>();
function emit(): void {
for (const listener of listeners) listener();
}
function activeSet(projectId: string): Set<string> {
let set = activeByProject.get(projectId);
if (!set) {
set = new Set();
activeByProject.set(projectId, set);
}
return set;
}
export function markProjectCliInteractionStarted(
projectId: string,
interactionId: string,
): void {
const set = activeSet(projectId);
const before = set.size;
set.add(interactionId);
if (set.size !== before) emit();
}
export function markProjectCliInteractionFinished(
projectId: string,
interactionId: string,
): void {
const set = activeByProject.get(projectId);
if (!set?.delete(interactionId)) return;
if (set.size === 0) activeByProject.delete(projectId);
emit();
}
export function markProjectCliInteractionStopped(
projectId: string,
interactionId: string,
): void {
markProjectCliInteractionFinished(projectId, interactionId);
}
export function hasProjectCliInteraction(projectId: string): boolean {
return (activeByProject.get(projectId)?.size ?? 0) > 0;
}
/**
* Drops optimistic interaction markers once the authoritative project snapshot
* confirms that no work remains. This is intentionally project-scoped: a
* terminal event received while another project is displayed must never leave
* the originating project's LED busy, nor clear activity in another project.
*/
export function reconcileProjectCliInteractionActivity(
projectId: string,
hasActiveWork: boolean,
): void {
if (hasActiveWork || !activeByProject.delete(projectId)) return;
emit();
}
export function subscribeProjectCliInteractionActivity(
listener: Listener,
): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
}
export function useProjectCliInteractionActivityVersion(): number {
const [version, setVersion] = useState(0);
useEffect(
() => subscribeProjectCliInteractionActivity(() => setVersion((v) => v + 1)),
[],
);
return version;
}
export function __resetProjectCliInteractionActivityForTests(): void {
activeByProject.clear();
emit();
}