diff --git a/client/src/components/run/TerminalView.tsx b/client/src/components/run/TerminalView.tsx
index bb48548..f16ff27 100644
--- a/client/src/components/run/TerminalView.tsx
+++ b/client/src/components/run/TerminalView.tsx
@@ -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);
}
};
diff --git a/client/src/components/run/__tests__/TerminalView.test.tsx b/client/src/components/run/__tests__/TerminalView.test.tsx
index 2cf1c76..dfda206 100644
--- a/client/src/components/run/__tests__/TerminalView.test.tsx
+++ b/client/src/components/run/__tests__/TerminalView.test.tsx
@@ -75,6 +75,15 @@ describe("TerminalView", () => {
expect(writeMock).toHaveBeenCalledWith("hello");
});
+ it("decodes binary ArrayBuffer frames (server sends PTY output as binary)", () => {
+ render();
+ 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();
const ws = MockWebSocket.instances[0]!;