Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b0bfc66d65 | |||
| 78fb82b257 |
@@ -29,18 +29,23 @@ export function TerminalView({ runId, wsBaseUrl }: TerminalViewProps) {
|
|||||||
fit.fit();
|
fit.fit();
|
||||||
|
|
||||||
const ws = new WebSocket(`${wsBaseUrl}/ws-pty/${encodeURIComponent(runId)}`);
|
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.onopen = () => {
|
||||||
ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
|
ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
|
||||||
};
|
};
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
if (typeof event.data === "string") {
|
const isArrayBuffer = Object.prototype.toString.call(event.data) === "[object ArrayBuffer]";
|
||||||
// Binary PTY output arrives as text here too (the browser WS API
|
const data = isArrayBuffer ? decoder.decode(event.data as ArrayBuffer) : event.data;
|
||||||
// decodes non-Blob/ArrayBuffer frames as strings) — a JSON control
|
if (typeof data === "string") {
|
||||||
// frame is the only thing that starts with `{"type"`.
|
// A JSON control frame is the only thing that starts with `{"type"`.
|
||||||
if (event.data.startsWith('{"type"')) {
|
if (data.startsWith('{"type"')) {
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(event.data);
|
const msg = JSON.parse(data);
|
||||||
if (msg.type === "exit") {
|
if (msg.type === "exit") {
|
||||||
term.write(`\r\n[session ended, exit code ${msg.code}]\r\n`);
|
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 */
|
/* 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");
|
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", () => {
|
it("forwards terminal keystrokes as outgoing WS sends", () => {
|
||||||
render(<TerminalView runId="ccam-lane-1" wsBaseUrl="ws://localhost:4820" />);
|
render(<TerminalView runId="ccam-lane-1" wsBaseUrl="ws://localhost:4820" />);
|
||||||
const ws = MockWebSocket.instances[0]!;
|
const ws = MockWebSocket.instances[0]!;
|
||||||
|
|||||||
@@ -894,7 +894,10 @@ export function Workspace() {
|
|||||||
key={l.id}
|
key={l.id}
|
||||||
lane={l}
|
lane={l}
|
||||||
selected={selectedLaneId === l.id}
|
selected={selectedLaneId === l.id}
|
||||||
onSelect={() => setSelectedLaneId(l.id)}
|
onSelect={() => {
|
||||||
|
setSelectedLaneId(l.id);
|
||||||
|
setCwd(l.cwd);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{!lanes.length && (
|
{!lanes.length && (
|
||||||
|
|||||||
Reference in New Issue
Block a user