fix(cli): les annonces MCP/inter-agent restent affichées pendant qu'un agent est busy — #232
This commit is contained in:
@ -31,6 +31,7 @@ import type {
|
|||||||
SlashCommand,
|
SlashCommand,
|
||||||
} from "@/domain";
|
} from "@/domain";
|
||||||
import { useGateways } from "@/app/di";
|
import { useGateways } from "@/app/di";
|
||||||
|
import { AnnouncementsPreview } from "@/features/announcements";
|
||||||
import { usePluginRuntime } from "@/features/plugins/PluginRuntimeProvider";
|
import { usePluginRuntime } from "@/features/plugins/PluginRuntimeProvider";
|
||||||
import {
|
import {
|
||||||
markProjectCliInteractionFinished,
|
markProjectCliInteractionFinished,
|
||||||
@ -127,6 +128,10 @@ interface ChatErrorState {
|
|||||||
openFailure: boolean;
|
openFailure: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RequesterDelegationState {
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
function describe(e: unknown): string {
|
function describe(e: unknown): string {
|
||||||
if (e && typeof e === "object" && "message" in e) {
|
if (e && typeof e === "object" && "message" in e) {
|
||||||
return String((e as GatewayError).message);
|
return String((e as GatewayError).message);
|
||||||
@ -842,7 +847,7 @@ export function CustomAgentChatView({
|
|||||||
onSessionId,
|
onSessionId,
|
||||||
onConversationId,
|
onConversationId,
|
||||||
}: CustomAgentChatViewProps) {
|
}: CustomAgentChatViewProps) {
|
||||||
const { agent, profile: profileGateway, system } = useGateways();
|
const { agent, profile: profileGateway, system, workState } = useGateways();
|
||||||
const pluginRuntime = usePluginRuntime();
|
const pluginRuntime = usePluginRuntime();
|
||||||
const initialConversationCacheKey = conversationCacheKey(
|
const initialConversationCacheKey = conversationCacheKey(
|
||||||
projectId,
|
projectId,
|
||||||
@ -1013,6 +1018,33 @@ export function CustomAgentChatView({
|
|||||||
[agentId, conversationId, projectId],
|
[agentId, conversationId, projectId],
|
||||||
);
|
);
|
||||||
const displayTurns = turns;
|
const displayTurns = turns;
|
||||||
|
const [requesterDelegation, setRequesterDelegation] =
|
||||||
|
useState<RequesterDelegationState>({ active: false });
|
||||||
|
|
||||||
|
const refreshRequesterDelegation = useCallback(async () => {
|
||||||
|
if (!workState) {
|
||||||
|
setRequesterDelegation({ active: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const state = await workState.getProjectWorkState(projectId);
|
||||||
|
const target = state.agents.find((row) =>
|
||||||
|
row.tickets.some(
|
||||||
|
(ticket) =>
|
||||||
|
ticket.status === "inProgress" &&
|
||||||
|
ticket.source.kind === "agent" &&
|
||||||
|
ticket.source.agentId === agentId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setRequesterDelegation(
|
||||||
|
target
|
||||||
|
? { active: true }
|
||||||
|
: { active: false },
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
setRequesterDelegation({ active: false });
|
||||||
|
}
|
||||||
|
}, [agentId, projectId, workState]);
|
||||||
|
|
||||||
const clearPersistedDraft = useCallback(async () => {
|
const clearPersistedDraft = useCallback(async () => {
|
||||||
if (!draftScope) return;
|
if (!draftScope) return;
|
||||||
@ -1034,6 +1066,36 @@ export function CustomAgentChatView({
|
|||||||
setDraft(text);
|
setDraft(text);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refreshRequesterDelegation();
|
||||||
|
}, [refreshRequesterDelegation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!system?.onDomainEvent) return;
|
||||||
|
let unsubscribe: (() => void) | undefined;
|
||||||
|
let cancelled = false;
|
||||||
|
void system
|
||||||
|
.onDomainEvent((event) => {
|
||||||
|
if (
|
||||||
|
event.type === "agentBusyChanged" ||
|
||||||
|
event.type === "delegationReady" ||
|
||||||
|
event.type === "orchestratorRequestProcessed" ||
|
||||||
|
event.type === "agentInboxChanged" ||
|
||||||
|
event.type === "agentWakeChanged"
|
||||||
|
) {
|
||||||
|
void refreshRequesterDelegation();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((un) => {
|
||||||
|
if (cancelled) un();
|
||||||
|
else unsubscribe = un;
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
unsubscribe?.();
|
||||||
|
};
|
||||||
|
}, [refreshRequesterDelegation, system]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mountedRef.current = true;
|
mountedRef.current = true;
|
||||||
return () => {
|
return () => {
|
||||||
@ -1914,7 +1976,7 @@ export function CustomAgentChatView({
|
|||||||
<div
|
<div
|
||||||
ref={chatRootRef}
|
ref={chatRootRef}
|
||||||
data-testid="custom-agent-chat-view"
|
data-testid="custom-agent-chat-view"
|
||||||
className="flex h-full min-h-0 flex-col overflow-hidden bg-surface text-content"
|
className="relative z-0 flex h-full min-h-0 flex-col overflow-hidden bg-surface text-content"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="toolbar"
|
role="toolbar"
|
||||||
@ -2076,6 +2138,13 @@ export function CustomAgentChatView({
|
|||||||
data-testid="custom-agent-chat-scroll"
|
data-testid="custom-agent-chat-scroll"
|
||||||
className="flex min-h-0 flex-1 basis-0 flex-col gap-2 overflow-x-hidden overflow-y-auto px-3 py-3"
|
className="flex min-h-0 flex-1 basis-0 flex-col gap-2 overflow-x-hidden overflow-y-auto px-3 py-3"
|
||||||
>
|
>
|
||||||
|
<div className="shrink-0">
|
||||||
|
<AnnouncementsPreview
|
||||||
|
requester={agentId}
|
||||||
|
fallbackActive={requesterDelegation.active}
|
||||||
|
fallbackText="Délégation en cours."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{opening && displayTurns.length === 0 ? (
|
{opening && displayTurns.length === 0 ? (
|
||||||
<div className="flex items-center gap-2 text-sm text-muted">
|
<div className="flex items-center gap-2 text-sm text-muted">
|
||||||
<Spinner size={14} />
|
<Spinner size={14} />
|
||||||
|
|||||||
@ -20,15 +20,21 @@ export interface AnnouncementsPreviewProps {
|
|||||||
ticketId?: string;
|
ticketId?: string;
|
||||||
/** How many most-recent announcements to show. */
|
/** How many most-recent announcements to show. */
|
||||||
limit?: number;
|
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({
|
export function AnnouncementsPreview({
|
||||||
requester,
|
requester,
|
||||||
ticketId,
|
ticketId,
|
||||||
limit = 3,
|
limit = 3,
|
||||||
|
fallbackActive = false,
|
||||||
|
fallbackText = "Délégation en cours.",
|
||||||
}: AnnouncementsPreviewProps) {
|
}: AnnouncementsPreviewProps) {
|
||||||
const announcements = useRequesterAnnouncements(requester, ticketId);
|
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).
|
// Most-recent `limit`, newest last (the list is oldest → newest).
|
||||||
const recent = announcements.slice(Math.max(0, announcements.length - limit));
|
const recent = announcements.slice(Math.max(0, announcements.length - limit));
|
||||||
@ -45,17 +51,23 @@ export function AnnouncementsPreview({
|
|||||||
/>
|
/>
|
||||||
En attente d'un agent
|
En attente d'un agent
|
||||||
</span>
|
</span>
|
||||||
<ul className="flex flex-col gap-0.5">
|
{recent.length === 0 ? (
|
||||||
{recent.map((a) => (
|
<p className="truncate text-xs text-muted" title={fallbackText}>
|
||||||
<li
|
{fallbackText}
|
||||||
key={a.seq}
|
</p>
|
||||||
className="truncate text-xs text-muted"
|
) : (
|
||||||
title={a.text}
|
<ul className="flex flex-col gap-0.5">
|
||||||
>
|
{recent.map((a) => (
|
||||||
{a.text}
|
<li
|
||||||
</li>
|
key={a.seq}
|
||||||
))}
|
className="truncate text-xs text-muted"
|
||||||
</ul>
|
title={a.text}
|
||||||
|
>
|
||||||
|
{a.text}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,6 +95,21 @@ describe("F3 — overlay lifecycle is the target's busy state", () => {
|
|||||||
expect(overlay()).not.toBeNull();
|
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 () => {
|
it("RETRACTS on busy:false even when NO completion event is emitted", async () => {
|
||||||
// The regression the arbitrage targets: a turn that ends without any
|
// The regression the arbitrage targets: a turn that ends without any
|
||||||
// completion signal (interruption / error / crash) must still bring the
|
// 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)", () => {
|
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 () => {
|
it("shows the requester's own announcements and hides another's", async () => {
|
||||||
const { system } = setup(<AnnouncementsPreview requester="agent-A" />);
|
const { system } = setup(<AnnouncementsPreview requester="agent-A" />);
|
||||||
await flush();
|
await flush();
|
||||||
|
|||||||
@ -26,6 +26,10 @@ export interface TargetAnnouncementsOverlayProps {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
/** The agent whose cell this overlay guards (the announcement `target`). */
|
/** The agent whose cell this overlay guards (the announcement `target`). */
|
||||||
agentId: string;
|
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 {
|
function formatTime(atMs: number): string {
|
||||||
@ -40,11 +44,14 @@ function formatTime(atMs: number): string {
|
|||||||
export function TargetAnnouncementsOverlay({
|
export function TargetAnnouncementsOverlay({
|
||||||
projectId,
|
projectId,
|
||||||
agentId,
|
agentId,
|
||||||
|
busyFallbackActive = false,
|
||||||
|
fallbackText = "Travaille sur une demande en cours.",
|
||||||
}: TargetAnnouncementsOverlayProps) {
|
}: TargetAnnouncementsOverlayProps) {
|
||||||
// Seed busy from the reconciled read-model so a turn already in flight surfaces
|
// 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.
|
// when this cell mounts (reboot / late open); live events then take over.
|
||||||
useHydrateAgentBusy(projectId, agentId);
|
useHydrateAgentBusy(projectId, agentId);
|
||||||
const { announcements, active } = useTargetAnnouncements(agentId);
|
const { announcements, active } = useTargetAnnouncements(agentId);
|
||||||
|
const effectiveActive = active || busyFallbackActive;
|
||||||
|
|
||||||
// Follow the tail as new announcements scroll in.
|
// Follow the tail as new announcements scroll in.
|
||||||
const feedRef = useRef<HTMLDivElement>(null);
|
const feedRef = useRef<HTMLDivElement>(null);
|
||||||
@ -53,7 +60,7 @@ export function TargetAnnouncementsOverlay({
|
|||||||
if (el) el.scrollTop = el.scrollHeight;
|
if (el) el.scrollTop = el.scrollHeight;
|
||||||
}, [announcements]);
|
}, [announcements]);
|
||||||
|
|
||||||
if (!active) return null;
|
if (!effectiveActive) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -75,20 +82,26 @@ export function TargetAnnouncementsOverlay({
|
|||||||
ref={feedRef}
|
ref={feedRef}
|
||||||
className="flex flex-1 flex-col justify-end gap-1.5 overflow-hidden px-6 py-4"
|
className="flex flex-1 flex-col justify-end gap-1.5 overflow-hidden px-6 py-4"
|
||||||
>
|
>
|
||||||
{announcements.map((a) => {
|
{announcements.length === 0 ? (
|
||||||
const time = formatTime(a.atMs);
|
<div className="flex items-center justify-center rounded-md border border-border/70 bg-surface/70 px-3 py-2 text-sm text-muted">
|
||||||
return (
|
{fallbackText}
|
||||||
<div
|
</div>
|
||||||
key={a.seq}
|
) : (
|
||||||
className="flex items-baseline gap-2 text-sm text-muted"
|
announcements.map((a) => {
|
||||||
>
|
const time = formatTime(a.atMs);
|
||||||
{time && <span className="shrink-0 text-xs text-faint">{time}</span>}
|
return (
|
||||||
<span className="whitespace-pre-wrap break-words text-content/90">
|
<div
|
||||||
{a.text}
|
key={a.seq}
|
||||||
</span>
|
className="flex items-baseline gap-2 text-sm text-muted"
|
||||||
</div>
|
>
|
||||||
);
|
{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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -96,6 +96,7 @@ async function seeded(profile: AgentProfile): Promise<Gateways> {
|
|||||||
const profileGateway = new MockProfileGateway();
|
const profileGateway = new MockProfileGateway();
|
||||||
const terminal = new MockTerminalGateway();
|
const terminal = new MockTerminalGateway();
|
||||||
const system = new MockSystemGateway();
|
const system = new MockSystemGateway();
|
||||||
|
const workState = new MockWorkStateGateway();
|
||||||
await profileGateway.configureProfiles([profile]);
|
await profileGateway.configureProfiles([profile]);
|
||||||
const created = await agent.createAgent("p1", {
|
const created = await agent.createAgent("p1", {
|
||||||
name: "Worker",
|
name: "Worker",
|
||||||
@ -114,6 +115,7 @@ async function seeded(profile: AgentProfile): Promise<Gateways> {
|
|||||||
profile: profileGateway,
|
profile: profileGateway,
|
||||||
terminal,
|
terminal,
|
||||||
system,
|
system,
|
||||||
|
workState,
|
||||||
} as unknown as Gateways;
|
} as unknown as Gateways;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,6 +182,98 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
|||||||
expect(screen.getAllByText(/mock-attachment.txt/).length).toBeGreaterThan(0);
|
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 () => {
|
it("persists custom CLI composer height per layout cell", async () => {
|
||||||
const rectSpy = vi
|
const rectSpy = vi
|
||||||
.spyOn(HTMLElement.prototype, "getBoundingClientRect")
|
.spyOn(HTMLElement.prototype, "getBoundingClientRect")
|
||||||
|
|||||||
@ -625,7 +625,7 @@ function LeafView({
|
|||||||
// F3 lifecycle state (busy) for this cell's agent — shared here so the
|
// 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
|
// 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.
|
// 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.
|
// Ticket #54 — local model-server launch state for this cell's pinned agent.
|
||||||
// Correlated `agentId → profileId → opencode.localModelServerId → status` via
|
// Correlated `agentId → profileId → opencode.localModelServerId → status` via
|
||||||
// the shared hook (same reduction as `useAgents`). While the bound server is
|
// 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 busyByWorkState = agentWork?.busy.state === "busy" || Boolean(activeTicket);
|
||||||
const delegatedTicket = activeTicket?.source.kind === "agent" ? activeTicket : undefined;
|
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 [restartPending, setRestartPending] = useState(false);
|
||||||
const [restartError, setRestartError] = useState(false);
|
const [restartError, setRestartError] = useState(false);
|
||||||
const [freshRestartedSession, setFreshRestartedSession] =
|
const [freshRestartedSession, setFreshRestartedSession] =
|
||||||
@ -1561,8 +1565,13 @@ function LeafView({
|
|||||||
busy state (agentBusyChanged + read-model hydration), never by the raw
|
busy state (agentBusyChanged + read-model hydration), never by the raw
|
||||||
PTY — it retracts at idle even when a turn ends without a completion
|
PTY — it retracts at idle even when a turn ends without a completion
|
||||||
event. Self-guards on `active` (busy) and renders null otherwise. */}
|
event. Self-guards on `active` (busy) and renders null otherwise. */}
|
||||||
{!modelServerOverlay && cellMode !== "custom" && agentId != null && (
|
{!modelServerOverlay && agentId != null && (
|
||||||
<TargetAnnouncementsOverlay projectId={projectId} agentId={agentId} />
|
<TargetAnnouncementsOverlay
|
||||||
|
projectId={projectId}
|
||||||
|
agentId={agentId}
|
||||||
|
busyFallbackActive={busyByWorkState}
|
||||||
|
fallbackText={targetAnnouncementsFallback}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{/* Ticket #54 — model-server launch veil. Top-priority full-cell overlay
|
{/* Ticket #54 — model-server launch veil. Top-priority full-cell overlay
|
||||||
(suppresses the write-portal + F3 veils above): while the pinned
|
(suppresses the write-portal + F3 veils above): while the pinned
|
||||||
|
|||||||
Reference in New Issue
Block a user