fix(cli): les annonces MCP/inter-agent restent affichées pendant qu'un agent est busy — #232
This commit is contained in:
@ -20,15 +20,21 @@ export interface AnnouncementsPreviewProps {
|
||||
ticketId?: string;
|
||||
/** How many most-recent announcements to show. */
|
||||
limit?: number;
|
||||
/** Read-model lifecycle fallback for a requester waiting without live text yet. */
|
||||
fallbackActive?: boolean;
|
||||
/** Stable copy shown when the fallback is active and no live text is available. */
|
||||
fallbackText?: string;
|
||||
}
|
||||
|
||||
export function AnnouncementsPreview({
|
||||
requester,
|
||||
ticketId,
|
||||
limit = 3,
|
||||
fallbackActive = false,
|
||||
fallbackText = "Délégation en cours.",
|
||||
}: AnnouncementsPreviewProps) {
|
||||
const announcements = useRequesterAnnouncements(requester, ticketId);
|
||||
if (announcements.length === 0) return null;
|
||||
if (announcements.length === 0 && !fallbackActive) return null;
|
||||
|
||||
// Most-recent `limit`, newest last (the list is oldest → newest).
|
||||
const recent = announcements.slice(Math.max(0, announcements.length - limit));
|
||||
@ -45,17 +51,23 @@ export function AnnouncementsPreview({
|
||||
/>
|
||||
En attente d'un agent
|
||||
</span>
|
||||
<ul className="flex flex-col gap-0.5">
|
||||
{recent.map((a) => (
|
||||
<li
|
||||
key={a.seq}
|
||||
className="truncate text-xs text-muted"
|
||||
title={a.text}
|
||||
>
|
||||
{a.text}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{recent.length === 0 ? (
|
||||
<p className="truncate text-xs text-muted" title={fallbackText}>
|
||||
{fallbackText}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="flex flex-col gap-0.5">
|
||||
{recent.map((a) => (
|
||||
<li
|
||||
key={a.seq}
|
||||
className="truncate text-xs text-muted"
|
||||
title={a.text}
|
||||
>
|
||||
{a.text}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -95,6 +95,21 @@ describe("F3 — overlay lifecycle is the target's busy state", () => {
|
||||
expect(overlay()).not.toBeNull();
|
||||
});
|
||||
|
||||
it("shows a stable busy fallback when no live announcement text exists", async () => {
|
||||
const { system } = setup(
|
||||
<TargetAnnouncementsOverlay
|
||||
projectId="p1"
|
||||
agentId="agent-T"
|
||||
fallbackText="Travaille sur une demande de Main."
|
||||
/>,
|
||||
);
|
||||
await flush();
|
||||
|
||||
act(() => system.emit(busy("agent-T", true)));
|
||||
|
||||
expect(screen.getByText("Travaille sur une demande de Main.")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("RETRACTS on busy:false even when NO completion event is emitted", async () => {
|
||||
// The regression the arbitrage targets: a turn that ends without any
|
||||
// completion signal (interruption / error / crash) must still bring the
|
||||
@ -209,6 +224,20 @@ describe("F3 — overlay lifecycle is the target's busy state", () => {
|
||||
});
|
||||
|
||||
describe("F2 — AnnouncementsPreview (requester filter)", () => {
|
||||
it("shows a requester-side fallback while waiting without live text", async () => {
|
||||
setup(
|
||||
<AnnouncementsPreview
|
||||
requester="agent-A"
|
||||
fallbackActive
|
||||
fallbackText="Délégation en cours."
|
||||
/>,
|
||||
);
|
||||
await flush();
|
||||
|
||||
expect(screen.getByTestId("announcements-preview")).not.toBeNull();
|
||||
expect(screen.getByText("Délégation en cours.")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("shows the requester's own announcements and hides another's", async () => {
|
||||
const { system } = setup(<AnnouncementsPreview requester="agent-A" />);
|
||||
await flush();
|
||||
|
||||
@ -26,6 +26,10 @@ export interface TargetAnnouncementsOverlayProps {
|
||||
projectId: string;
|
||||
/** The agent whose cell this overlay guards (the announcement `target`). */
|
||||
agentId: string;
|
||||
/** Reconciled busy state from the caller's read-model, used as lifecycle fallback. */
|
||||
busyFallbackActive?: boolean;
|
||||
/** Stable copy shown while the busy cycle has no live announcement content yet. */
|
||||
fallbackText?: string;
|
||||
}
|
||||
|
||||
function formatTime(atMs: number): string {
|
||||
@ -40,11 +44,14 @@ function formatTime(atMs: number): string {
|
||||
export function TargetAnnouncementsOverlay({
|
||||
projectId,
|
||||
agentId,
|
||||
busyFallbackActive = false,
|
||||
fallbackText = "Travaille sur une demande en cours.",
|
||||
}: TargetAnnouncementsOverlayProps) {
|
||||
// Seed busy from the reconciled read-model so a turn already in flight surfaces
|
||||
// when this cell mounts (reboot / late open); live events then take over.
|
||||
useHydrateAgentBusy(projectId, agentId);
|
||||
const { announcements, active } = useTargetAnnouncements(agentId);
|
||||
const effectiveActive = active || busyFallbackActive;
|
||||
|
||||
// Follow the tail as new announcements scroll in.
|
||||
const feedRef = useRef<HTMLDivElement>(null);
|
||||
@ -53,7 +60,7 @@ export function TargetAnnouncementsOverlay({
|
||||
if (el) el.scrollTop = el.scrollHeight;
|
||||
}, [announcements]);
|
||||
|
||||
if (!active) return null;
|
||||
if (!effectiveActive) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -75,20 +82,26 @@ export function TargetAnnouncementsOverlay({
|
||||
ref={feedRef}
|
||||
className="flex flex-1 flex-col justify-end gap-1.5 overflow-hidden px-6 py-4"
|
||||
>
|
||||
{announcements.map((a) => {
|
||||
const time = formatTime(a.atMs);
|
||||
return (
|
||||
<div
|
||||
key={a.seq}
|
||||
className="flex items-baseline gap-2 text-sm text-muted"
|
||||
>
|
||||
{time && <span className="shrink-0 text-xs text-faint">{time}</span>}
|
||||
<span className="whitespace-pre-wrap break-words text-content/90">
|
||||
{a.text}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{announcements.length === 0 ? (
|
||||
<div className="flex items-center justify-center rounded-md border border-border/70 bg-surface/70 px-3 py-2 text-sm text-muted">
|
||||
{fallbackText}
|
||||
</div>
|
||||
) : (
|
||||
announcements.map((a) => {
|
||||
const time = formatTime(a.atMs);
|
||||
return (
|
||||
<div
|
||||
key={a.seq}
|
||||
className="flex items-baseline gap-2 text-sm text-muted"
|
||||
>
|
||||
{time && <span className="shrink-0 text-xs text-faint">{time}</span>}
|
||||
<span className="whitespace-pre-wrap break-words text-content/90">
|
||||
{a.text}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user