fix(run): decode binary WS frames in TerminalView so pty output actually renders

Server streams PTY output as binary WS frames, but the browser's default
binaryType ("blob") handed onmessage a Blob that never matched the
`typeof === "string"` check — every keystroke response was silently
dropped and the terminal stayed blank despite the backend streaming
correctly (verified via a raw ws client against the live tmux session).
This commit is contained in:
2026-08-13 13:54:19 +07:00
parent 7e2bb6225f
commit 78fb82b257
2 changed files with 21 additions and 7 deletions
+12 -7
View File
@@ -29,18 +29,23 @@ export function TerminalView({ runId, wsBaseUrl }: TerminalViewProps) {
fit.fit();
const ws = new WebSocket(`${wsBaseUrl}/ws-pty/${encodeURIComponent(runId)}`);
// Server sends PTY bytes as binary frames — default binaryType ("blob")
// would hand onmessage a Blob that the string checks below never match,
// silently dropping all terminal output. "arraybuffer" keeps it sync.
ws.binaryType = "arraybuffer";
const decoder = new TextDecoder();
ws.onopen = () => {
ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
};
ws.onmessage = (event) => {
if (typeof event.data === "string") {
// Binary PTY output arrives as text here too (the browser WS API
// decodes non-Blob/ArrayBuffer frames as strings) — a JSON control
// frame is the only thing that starts with `{"type"`.
if (event.data.startsWith('{"type"')) {
const isArrayBuffer = Object.prototype.toString.call(event.data) === "[object ArrayBuffer]";
const data = isArrayBuffer ? decoder.decode(event.data as ArrayBuffer) : event.data;
if (typeof data === "string") {
// A JSON control frame is the only thing that starts with `{"type"`.
if (data.startsWith('{"type"')) {
try {
const msg = JSON.parse(event.data);
const msg = JSON.parse(data);
if (msg.type === "exit") {
term.write(`\r\n[session ended, exit code ${msg.code}]\r\n`);
}
@@ -49,7 +54,7 @@ export function TerminalView({ runId, wsBaseUrl }: TerminalViewProps) {
/* not JSON — fall through and render as PTY output */
}
}
term.write(event.data);
term.write(data);
}
};
@@ -75,6 +75,15 @@ describe("TerminalView", () => {
expect(writeMock).toHaveBeenCalledWith("hello");
});
it("decodes binary ArrayBuffer frames (server sends PTY output as binary)", () => {
render(<TerminalView runId="ccam-lane-1" wsBaseUrl="ws://localhost:4820" />);
const ws = MockWebSocket.instances[0]!;
ws.onopen?.();
const bytes = new TextEncoder().encode("hello-binary").buffer;
ws.onmessage?.({ data: bytes });
expect(writeMock).toHaveBeenCalledWith("hello-binary");
});
it("forwards terminal keystrokes as outgoing WS sends", () => {
render(<TerminalView runId="ccam-lane-1" wsBaseUrl="ws://localhost:4820" />);
const ws = MockWebSocket.instances[0]!;