fix(frontend): refresh immédiat de la liste des agents après ajout — bus d'événements agent-roster (CustomEvent + BroadcastChannel multi-fenêtres) publié à la création d'un agent et écouté par useAgents, LayoutGrid et les panneaux skills/permissions/tâches planifiées/templates/tickets/workstate, plus besoin d'ajouter-retirer une cellule pour forcer le refresh — #361 (QA verte: typecheck OK, vitest agents/singletonAgent OK, npm test 1416 tests OK)
This commit is contained in:
@ -24,6 +24,7 @@ import { useDrift } from "@/features/templates/useDrift";
|
||||
import { templateProfileLabel } from "@/features/templates/templatePresentation";
|
||||
import { useGateways } from "@/app/di";
|
||||
import { useAgents } from "./useAgents";
|
||||
import { publishAgentRosterChanged } from "./agentRosterEvents";
|
||||
import { correlateModelServerStatus } from "./modelServerLaunch";
|
||||
import { AgentLimitBadge } from "./AgentLimitBadge";
|
||||
import { ModelServerLaunchBadge } from "./ModelServerLaunchBadge";
|
||||
@ -206,6 +207,7 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
name: newName.trim(),
|
||||
synchronized: true,
|
||||
});
|
||||
publishAgentRosterChanged(projectId);
|
||||
// Refresh the agents list since we bypassed the hook's createAgent path.
|
||||
await vm.refresh();
|
||||
} else {
|
||||
|
||||
48
frontend/src/features/agents/agentRosterEvents.ts
Normal file
48
frontend/src/features/agents/agentRosterEvents.ts
Normal file
@ -0,0 +1,48 @@
|
||||
export const AGENT_ROSTER_CHANGED_EVENT = "idea:agent-roster-changed";
|
||||
const AGENT_ROSTER_CHANNEL = "idea:agent-roster";
|
||||
|
||||
export interface AgentRosterChanged {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
let agentRosterChannel: BroadcastChannel | null | undefined;
|
||||
|
||||
function sharedAgentRosterChannel(): BroadcastChannel | null {
|
||||
if (agentRosterChannel !== undefined) return agentRosterChannel;
|
||||
agentRosterChannel =
|
||||
typeof BroadcastChannel === "undefined"
|
||||
? null
|
||||
: new BroadcastChannel(AGENT_ROSTER_CHANNEL);
|
||||
return agentRosterChannel;
|
||||
}
|
||||
|
||||
/** Invalidates every open frontend projection of a project's agent roster. */
|
||||
export function publishAgentRosterChanged(projectId: string): void {
|
||||
const change = { projectId };
|
||||
window.dispatchEvent(
|
||||
new CustomEvent<AgentRosterChanged>(AGENT_ROSTER_CHANGED_EVENT, {
|
||||
detail: change,
|
||||
}),
|
||||
);
|
||||
sharedAgentRosterChannel()?.postMessage(change);
|
||||
}
|
||||
|
||||
/** Subscribes in this window and in sibling detached webviews. */
|
||||
export function onAgentRosterChanged(
|
||||
handler: (change: AgentRosterChanged) => void,
|
||||
): () => void {
|
||||
const onWindowEvent = (event: Event) => {
|
||||
const detail = (event as CustomEvent<AgentRosterChanged>).detail;
|
||||
if (detail?.projectId) handler(detail);
|
||||
};
|
||||
const onChannelMessage = (event: MessageEvent<AgentRosterChanged>) => {
|
||||
if (event.data?.projectId) handler(event.data);
|
||||
};
|
||||
const channel = sharedAgentRosterChannel();
|
||||
window.addEventListener(AGENT_ROSTER_CHANGED_EVENT, onWindowEvent);
|
||||
channel?.addEventListener("message", onChannelMessage);
|
||||
return () => {
|
||||
window.removeEventListener(AGENT_ROSTER_CHANGED_EVENT, onWindowEvent);
|
||||
channel?.removeEventListener("message", onChannelMessage);
|
||||
};
|
||||
}
|
||||
@ -29,6 +29,7 @@ import {
|
||||
import type { Gateways } from "@/ports";
|
||||
import { DIProvider } from "@/app/di";
|
||||
import { AgentsPanel } from "./AgentsPanel";
|
||||
import { AGENT_ROSTER_CHANGED_EVENT } from "./agentRosterEvents";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@ -130,6 +131,20 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
expect(agent).toBeTruthy();
|
||||
});
|
||||
|
||||
it("invalidates the project agent roster after successful creation", async () => {
|
||||
const changed = vi.fn();
|
||||
window.addEventListener(AGENT_ROSTER_CHANGED_EVENT, changed);
|
||||
renderPanel();
|
||||
|
||||
await createAgent("Fresh Worker");
|
||||
|
||||
await waitFor(() => expect(changed).toHaveBeenCalledTimes(1));
|
||||
expect((changed.mock.calls[0][0] as CustomEvent).detail).toEqual({
|
||||
projectId: PROJECT_ID,
|
||||
});
|
||||
window.removeEventListener(AGENT_ROSTER_CHANGED_EVENT, changed);
|
||||
});
|
||||
|
||||
it("shows the resolved network permission badge for an agent", async () => {
|
||||
const agent = new MockAgentGateway();
|
||||
const permission = new MockPermissionGateway();
|
||||
|
||||
@ -19,6 +19,10 @@ import type {
|
||||
} from "@/domain";
|
||||
import type { LiveAgent, OpenTerminalOptions, TerminalHandle } from "@/ports";
|
||||
import { useGateways } from "@/app/di";
|
||||
import {
|
||||
onAgentRosterChanged,
|
||||
publishAgentRosterChanged,
|
||||
} from "./agentRosterEvents";
|
||||
|
||||
/**
|
||||
* Per-agent session-limit state (ARCHITECTURE §21), populated from the limit
|
||||
@ -223,6 +227,14 @@ export function useAgents(projectId: string): AgentsViewModel {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
onAgentRosterChanged((change) => {
|
||||
if (change.projectId === projectId) void refresh();
|
||||
}),
|
||||
[projectId, refresh],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshLiveAgents();
|
||||
}, [refreshLiveAgents]);
|
||||
@ -360,6 +372,7 @@ export function useAgents(projectId: string): AgentsViewModel {
|
||||
initialContent,
|
||||
});
|
||||
setAgents((prev) => [...prev, created]);
|
||||
publishAgentRosterChanged(projectId);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user