Compare commits

...

2 Commits

Author SHA1 Message Date
nntrivi2001 b0bfc66d65 fix(workspace): auto-set cwd when a lane is selected from the strip
Selecting a lane card only updated selectedLaneId, leaving the run-setup
cwd field on whatever it was before — now selecting a lane also syncs
cwd to that lane's own working directory.
2026-08-13 13:55:51 +07:00
nntrivi2001 78fb82b257 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).
2026-08-13 13:54:19 +07:00
3 changed files with 25 additions and 8 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]!;
+4 -1
View File
@@ -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 && (