feat(frontend): surface agent web sur WebSocket (cellule agent) (#13)
Lot F4 du chantier server/client mode : le client web expose une cellule agent branchée sur le PTY WebSocket, en face de agent.launch (B6). - wsLiveClient.ts : launchAgent sur le transport WebSocket. - streamGateways.ts : gateway agent alignée sur le contrat serveur B6. - WebAgentCell.tsx : cellule agent web, câblée dans WebWorkspace et index. - Tests : agentGateway.test.ts, WebApp.test.tsx. Validé : frontend 749 tests verts, contrat B6↔F4 aligné (aucun écart de frame), desktop non régressé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -52,22 +52,9 @@ import type {
|
||||
} from "@/ports";
|
||||
import type { HttpInvoker } from "./httpInvoker";
|
||||
import { WsLiveClient } from "./wsLiveClient";
|
||||
import { base64ToBytes, type AttachedPayload, type ChatOutputPayload } from "./frames";
|
||||
import { type ChatOutputPayload } from "./frames";
|
||||
import { unsupportedOnWeb } from "./unsupported";
|
||||
|
||||
/** Concatenates a scrollback frame list into a single byte buffer. */
|
||||
function attachedToScrollback(payload: AttachedPayload): Uint8Array {
|
||||
const chunks = payload.scrollback.map((c) => base64ToBytes(c.bytesBase64));
|
||||
const total = chunks.reduce((n, c) => n + c.length, 0);
|
||||
const out = new Uint8Array(total);
|
||||
let offset = 0;
|
||||
for (const c of chunks) {
|
||||
out.set(c, offset);
|
||||
offset += c.length;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link TerminalHandle} whose control operations are WS frames. The
|
||||
* output stream is delivered through the sink the gateway registered on the
|
||||
@ -235,34 +222,34 @@ export class HttpAgentGateway implements AgentGateway {
|
||||
options: OpenTerminalOptions,
|
||||
onData: (bytes: Uint8Array) => void,
|
||||
): Promise<TerminalHandle> {
|
||||
// Raw-CLI agents stream over the same WS PTY channel (B0). Structured
|
||||
// sessions do not use this channel — that branch is B6.
|
||||
const ack = await this.ws.send("agent.launch", {
|
||||
// B6: a raw-CLI agent streams over the same PTY channel as a terminal. The
|
||||
// WS client tracks the session (reconnection/replay/status parity with F3);
|
||||
// the unified `terminal.attached` ack yields the sessionId + the conversation
|
||||
// id minted by this launch. Structured agents are refused server-side with
|
||||
// `UNSUPPORTED` — the rejection propagates to the caller (a clear cell error,
|
||||
// no crash). A fresh launch has empty scrollback (no repaint on open).
|
||||
const res = await this.ws.launchAgent({
|
||||
projectId,
|
||||
agentId,
|
||||
nodeId: options.nodeId ?? null,
|
||||
rows: options.rows,
|
||||
cols: options.cols,
|
||||
conversationId: options.conversationId ?? null,
|
||||
onData,
|
||||
});
|
||||
const payload = ack.payload as unknown as AttachedPayload;
|
||||
const sessionId = payload.session.sessionId;
|
||||
this.ws.setOutputSink(sessionId, onData);
|
||||
const scrollback = attachedToScrollback(payload);
|
||||
if (scrollback.length > 0) onData(scrollback);
|
||||
return makeWsTerminalHandle(sessionId, this.ws, payload.assignedConversationId);
|
||||
return makeWsTerminalHandle(res.sessionId, this.ws, res.assignedConversationId);
|
||||
}
|
||||
|
||||
async reattach(
|
||||
sessionId: string,
|
||||
onData: (bytes: Uint8Array) => void,
|
||||
): Promise<ReattachResult> {
|
||||
const ack = await this.ws.send("terminal.attach", { sessionId, lastSeq: null });
|
||||
const payload = ack.payload as unknown as AttachedPayload;
|
||||
this.ws.setOutputSink(sessionId, onData);
|
||||
// Agent sessions re-attach through the same `terminal.attach` mechanics as a
|
||||
// terminal — no relaunch, bounded scrollback replayed for the view to repaint.
|
||||
const res = await this.ws.attachTerminal({ sessionId, onData });
|
||||
return {
|
||||
handle: makeWsTerminalHandle(sessionId, this.ws),
|
||||
scrollback: attachedToScrollback(payload),
|
||||
handle: makeWsTerminalHandle(res.sessionId, this.ws),
|
||||
scrollback: res.scrollback,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user