fix(cli): les annonces MCP/inter-agent restent affichées pendant qu'un agent est busy — #232

This commit is contained in:
2026-08-21 19:40:13 +02:00
parent a51813fc44
commit aa85d4dfc8
6 changed files with 258 additions and 32 deletions

View File

@ -96,6 +96,7 @@ async function seeded(profile: AgentProfile): Promise<Gateways> {
const profileGateway = new MockProfileGateway();
const terminal = new MockTerminalGateway();
const system = new MockSystemGateway();
const workState = new MockWorkStateGateway();
await profileGateway.configureProfiles([profile]);
const created = await agent.createAgent("p1", {
name: "Worker",
@ -114,6 +115,7 @@ async function seeded(profile: AgentProfile): Promise<Gateways> {
profile: profileGateway,
terminal,
system,
workState,
} as unknown as Gateways;
}
@ -180,6 +182,98 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
expect(screen.getAllByText(/mock-attachment.txt/).length).toBeGreaterThan(0);
});
it("keeps the target announcement overlay visible in custom mode while busy (#232)", async () => {
const gateways = await seeded(structuredProfile);
const layout = gateways.layout as MockLayoutGateway;
const workState = gateways.workState as MockWorkStateGateway;
const tree = await layout.loadLayout("p1");
const leaf = leaves(tree)[0];
const agentId = leaf.agent;
if (!agentId) throw new Error("expected seeded agent");
window.localStorage.setItem(`idea.agent-cell-mode.p1.${leaf.id}`, "custom");
workState._setProjectWorkState("p1", {
agents: [
{
agentId,
name: "Worker",
profileId: structuredProfile.id,
busy: { state: "busy", ticket: "ticket-232", sinceMs: 1 },
tickets: [
{
ticketId: "ticket-232",
conversationId: "conversation-232",
position: 0,
status: "inProgress",
source: { kind: "agent", agentId: "main" },
requesterLabel: "Main",
taskPreview: "Analyse",
taskLen: 7,
},
],
},
],
conversations: [],
});
renderGrid(gateways);
expect(await screen.findByTestId("custom-agent-chat-view")).toBeTruthy();
expect(await screen.findByTestId("target-announcements-overlay")).toBeTruthy();
expect(screen.getByText("Travaille sur une demande de Main.")).toBeTruthy();
});
it("renders requester-side delegation fallback in custom mode after remount (#232)", async () => {
const gateways = await seeded(structuredProfile);
const layout = gateways.layout as MockLayoutGateway;
const workState = gateways.workState as MockWorkStateGateway;
const tree = await layout.loadLayout("p1");
const leaf = leaves(tree)[0];
const requesterId = leaf.agent;
if (!requesterId) throw new Error("expected seeded requester");
window.localStorage.setItem(`idea.agent-cell-mode.p1.${leaf.id}`, "custom");
workState._setProjectWorkState("p1", {
agents: [
{
agentId: requesterId,
name: "Worker",
profileId: structuredProfile.id,
busy: { state: "idle" },
tickets: [],
},
{
agentId: "target-agent",
name: "Architect",
profileId: structuredProfile.id,
busy: { state: "busy", ticket: "ticket-232", sinceMs: 1 },
tickets: [
{
ticketId: "ticket-232",
conversationId: "conversation-232",
position: 0,
status: "inProgress",
source: { kind: "agent", agentId: requesterId },
requesterLabel: "Worker",
taskPreview: "Répondre",
taskLen: 8,
},
],
},
],
conversations: [],
});
const view = renderGrid(gateways);
expect(await screen.findByTestId("custom-agent-chat-view")).toBeTruthy();
expect(await screen.findByText("Délégation en cours.")).toBeTruthy();
view.unmount();
renderGrid(gateways);
expect(await screen.findByTestId("custom-agent-chat-view")).toBeTruthy();
expect(await screen.findByText("Délégation en cours.")).toBeTruthy();
});
it("persists custom CLI composer height per layout cell", async () => {
const rectSpy = vi
.spyOn(HTMLElement.prototype, "getBoundingClientRect")

View File

@ -625,7 +625,7 @@ function LeafView({
// F3 lifecycle state (busy) for this cell's agent — shared here so the
// write-portal veil can be mutually excluded with the F3 overlay (exactly one
// full-cell veil at a time, F3 first). Same source the overlay itself reads.
const { active: busyActive } = useTargetAnnouncements(agentId ?? "");
const { active: announcementBusyActive } = useTargetAnnouncements(agentId ?? "");
// Ticket #54 — local model-server launch state for this cell's pinned agent.
// Correlated `agentId → profileId → opencode.localModelServerId → status` via
// the shared hook (same reduction as `useAgents`). While the bound server is
@ -768,6 +768,10 @@ function LeafView({
) ?? [];
const busyByWorkState = agentWork?.busy.state === "busy" || Boolean(activeTicket);
const delegatedTicket = activeTicket?.source.kind === "agent" ? activeTicket : undefined;
const busyActive = announcementBusyActive || busyByWorkState;
const targetAnnouncementsFallback = delegatedTicket?.requesterLabel
? `Travaille sur une demande de ${delegatedTicket.requesterLabel}.`
: "Travaille sur une demande en cours.";
const [restartPending, setRestartPending] = useState(false);
const [restartError, setRestartError] = useState(false);
const [freshRestartedSession, setFreshRestartedSession] =
@ -1561,8 +1565,13 @@ function LeafView({
busy state (agentBusyChanged + read-model hydration), never by the raw
PTY — it retracts at idle even when a turn ends without a completion
event. Self-guards on `active` (busy) and renders null otherwise. */}
{!modelServerOverlay && cellMode !== "custom" && agentId != null && (
<TargetAnnouncementsOverlay projectId={projectId} agentId={agentId} />
{!modelServerOverlay && agentId != null && (
<TargetAnnouncementsOverlay
projectId={projectId}
agentId={agentId}
busyFallbackActive={busyByWorkState}
fallbackText={targetAnnouncementsFallback}
/>
)}
{/* Ticket #54 — model-server launch veil. Top-priority full-cell overlay
(suppresses the write-portal + F3 veils above): while the pinned