feat(frontend): xterm.js sur WebSocket avec reconnexion et scrollback (#13)
Lot F3 du chantier server/client mode : le client web branche xterm.js sur le terminal distant via WebSocket, en face de l'endpoint PTY B5. - wsLiveClient.ts : transport WebSocket du terminal avec reconnexion. - streamGateways.ts : gateway terminal (open/attach/data/resize/close). - frames.ts : frames PTY alignées sur le contrat serveur B5. - Tests : terminalGateway.test.ts, wsLiveClientReconnect.test.ts. Validé : frontend 743 tests verts (F3 ciblé 12), cohérence des frames B5↔F3 confirmée, desktop non régressé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -93,12 +93,11 @@ export function makeWsTerminalHandle(
|
||||
detach(): void {
|
||||
// Stop delivering output locally and tell the server the view is gone. The
|
||||
// backend PTY keeps running (detach ≠ close), matching the Tauri handle.
|
||||
ws.removeOutputSink(sessionId);
|
||||
void ws.sendFireAndForget("terminal.detach", { sessionId });
|
||||
// Untracking also disables reconnection re-attach for this session.
|
||||
void ws.detachTerminal(sessionId);
|
||||
},
|
||||
async close(): Promise<void> {
|
||||
ws.removeOutputSink(sessionId);
|
||||
await ws.send("terminal.close", { sessionId });
|
||||
await ws.closeTerminalSession(sessionId);
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -137,37 +136,33 @@ export class HttpTerminalGateway implements TerminalGateway {
|
||||
options: OpenTerminalOptions,
|
||||
onData: (bytes: Uint8Array) => void,
|
||||
): Promise<TerminalHandle> {
|
||||
const ack = await this.ws.send("terminal.open", {
|
||||
projectId: undefined, // TODO(F3/B5): the port lacks projectId; confirm contract.
|
||||
nodeId: options.nodeId ?? null,
|
||||
// B5: `terminal.open` carries no projectId; `cwd` is a server-validated path.
|
||||
// A fresh open has empty scrollback (the view does not repaint on open).
|
||||
const res = await this.ws.openTerminal({
|
||||
cwd: options.cwd,
|
||||
rows: options.rows,
|
||||
cols: options.cols,
|
||||
nodeId: options.nodeId ?? 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);
|
||||
return makeWsTerminalHandle(res.sessionId, this.ws);
|
||||
}
|
||||
|
||||
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);
|
||||
// The view repaints the returned scrollback itself (TerminalView contract);
|
||||
// the client does not also push it through `onData` on the initial attach.
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
async closeTerminal(sessionId: string): Promise<void> {
|
||||
this.ws.removeOutputSink(sessionId);
|
||||
await this.ws.send("terminal.close", { sessionId });
|
||||
await this.ws.closeTerminalSession(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user