import { useEffect, useState } from "react"; type Listener = () => void; const activeByProject = new Map>(); const listeners = new Set(); function emit(): void { for (const listener of listeners) listener(); } function activeSet(projectId: string): Set { 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(); }