feat(ui): LED projet — exploite les signaux d'interaction CLI custom (#198, QA verte)

Projette l'état d'interaction agent→projet sur la LED via des signaux explicites de la CLI custom:

- projectCliInteractionActivity: modèle d'activité d'interaction (InteractionStarted/Finished/Stopped, interactionId/seq, version).

- useProjectActivity + ProjectTabs: projection live « interaction en cours » → LED, retour immédiat à l'état attendu au stop ou à la fin réelle de l'échange (pas d'état collant).

- CustomAgentChatView: émet les signaux d'interaction CLI custom (début d'envoi, fin de tour, stop) consommés par l'activité projet — sans dériver la LED du texte streamé ou d'heuristiques.

Tout côté frontend: aucun signal runtime manquant (les signaux proviennent de la CLI custom).

Tests QA verts: projectActivity.test.ts, ProjectTabs.test.tsx (+ useProjectActivity.test.tsx, CustomAgentChatView.test.tsx).
This commit is contained in:
2026-08-12 15:44:30 +02:00
parent b38f5b6f14
commit b167226179
8 changed files with 226 additions and 14 deletions

View File

@ -0,0 +1,71 @@
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;
}
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();
}