Compare commits
2 Commits
7e2bb6225f
...
b0bfc66d65
| Author | SHA1 | Date | |
|---|---|---|---|
| b0bfc66d65 | |||
| 78fb82b257 |
@@ -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]!;
|
||||
|
||||
@@ -894,7 +894,10 @@ export function Workspace() {
|
||||
key={l.id}
|
||||
lane={l}
|
||||
selected={selectedLaneId === l.id}
|
||||
onSelect={() => setSelectedLaneId(l.id)}
|
||||
onSelect={() => {
|
||||
setSelectedLaneId(l.id);
|
||||
setCwd(l.cwd);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{!lanes.length && (
|
||||
|
||||
Reference in New Issue
Block a user