feat(frontend): surfaces live web (workstate live, background, inbox, reconnect) (#13)

Lot F5 du chantier server/client mode : le client web consomme les frames
event.domain (B7) pour animer ses surfaces live.

- webLive.ts : consommation du flux event.domain (workstate, background,
  inbox).
- useLiveReconnect.ts : reconnexion du flux live.
- wsLiveClient.ts / index.ts : câblage du transport live.
- WebWorkspace.tsx : surfaces live branchées.
- Tests : WebWorkspaceLive.test.tsx, wsLiveClientReconnect.test.ts,
  WebApp.test.tsx.

Validé : frontend 753 tests verts, desktop non régressé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 23:37:02 +02:00
parent 1bc5217dbc
commit dd1d083a1a
8 changed files with 457 additions and 112 deletions

View File

@ -158,6 +158,29 @@ describe("WsLiveClient connection state + reconnection", () => {
expect(h.hasTimer()).toBe(false);
});
it("reconnects for a live domain-event subscription even with no terminal (F5)", async () => {
const h = harness();
// A live surface subscribes to domain events (no terminal open).
h.ws.setDomainEventHandler(() => {});
await h.ws.ensureConnected();
await vi.waitFor(() => expect(h.ws.getConnectionState()).toBe("connected"));
// Drop the socket → must schedule a reconnect (the subscription still needs it).
h.sockets[0].close();
expect(h.ws.getConnectionState()).toBe("reconnecting");
expect(h.hasTimer()).toBe(true);
h.fireTimer();
await vi.waitFor(() => expect(h.sockets.length).toBe(2));
await vi.waitFor(() => expect(h.ws.getConnectionState()).toBe("connected"));
// Events resume flowing to the persisted handler after reconnect.
const events: unknown[] = [];
h.ws.setDomainEventHandler((e) => events.push(e));
h.sockets[1].receive({ kind: "event.domain", payload: { type: "agentBusyChanged", agentId: "a1", busy: true } });
expect(events).toHaveLength(1);
});
it("dispose() moves to closed and clears any pending reconnect", async () => {
const h = harness();
await attach(h.ws, h.sockets, "s1", () => {});