Merge pull request #431 from rounakdatta/feat/mobile-terminal-resilience

fix(terminal): four silent-failure paths — renderer freeze, replay race, reconnect gap, unbounded fetches
This commit is contained in:
Codeman maintainer
2026-09-23 11:32:14 +02:00
33 changed files with 2221 additions and 172 deletions
+7 -1
View File
@@ -2124,7 +2124,13 @@ export function registerSessionRoutes(
const session = findSessionOrFail(ctx, id, req);
session.resize(cols, rows, { viewportType, force });
return {};
// Answer with the geometry the PTY ACTUALLY holds, which is not always the
// one asked for: `Session.resize` declines small-viewport requests while a
// desktop connection holds an active sizing claim. A browser terminal left
// at a shape the PTY refused renders garbled output, not merely wrong-sized
// output, so the client adopts this (issue #464). A session with no pane
// reports nothing rather than the constructor defaults — see `ptyGeometry`.
return session.ptyGeometry ?? {};
});
// ========== Get Last Response (from transcript JSONL) ==========
+16
View File
@@ -22,6 +22,9 @@
* {"t":"c"} — clear terminal
* {"t":"r"} — needs refresh (reload buffer)
* {"t":"ia","seq":N} — input ACK (echoes the seq of an applied/deduped input frame)
* {"t":"zc","c":N,"r":N} — resize confirm: the geometry the PTY now holds, which
* is NOT always the one requested (see Session.resize
* arbitration). Clients adopt it — issue #464.
* Client -> Server:
* {"t":"i","d":"...","seq":N,"cid":"..."} — input (keystroke or paste). seq+cid are
* optional reliable-delivery tags: the server applies each
@@ -240,6 +243,19 @@ export function registerWsRoutes(app: FastifyInstance, ctx: SessionPort, getHost
}
const force = msg.f === true;
session.resize(msg.c, msg.r, { viewportType, force });
// Report the geometry that actually took. Resize used to be
// write-only, so a client whose request was declined by the
// arbitration above — or floored, or overridden by another device
// — had no way to find out, and went on rendering a CLI's repaints
// against a screen shape that did not exist (issue #464). Sent
// unconditionally: it is ~30 bytes on a debounced, rare message,
// and always-send means the client needs no "did it take?" state.
// A session with no pane sends nothing at all: its `_ptyCols`/
// `_ptyRows` are constructor defaults no process was ever told.
const applied = session.ptyGeometry;
if (applied && socket.readyState === 1) {
socket.send(`{"t":"zc","c":${applied.cols},"r":${applied.rows}}`);
}
}
} catch {
// Ignore malformed messages