fix(v0.4): full-screen per device — latest-writer-wins PTY sizing
A shared PTY can only be one size; min-sizing clamped the shared session to the SMALLEST viewer, so a wide desktop got letterboxed when an iPad was also attached (iPad looked full-screen, desktop did not). Switch to latest-writer-wins: the device that most recently fit/focused drives the PTY size, so whichever device you're actively using is full-screen. - session.ts: setClientDims resizes the PTY directly (drop min across clients); attach/detach/blur never resize (the active device keeps its size) - frontend: TerminalSession.refit() force-resends dims; window 'focus' / visibilitychange re-assert the active tab's size, so switching devices reclaims full-screen - tests updated for latest-wins (incl. join/hidden/blur don't resize) Verified (two ws clients): 200x50 → iPad 100x40 → desktop refit 200x50 → iPad blur keeps 200x50. 225 tests green.
This commit is contained in:
@@ -52,21 +52,9 @@ export function broadcast(session: Session, msg: ServerMessage): void {
|
||||
for (const ws of session.clients) sendIfOpen(ws, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the PTY to the MIN cols/rows across all attached clients (tmux-style),
|
||||
* so a small phone and a wide laptop sharing the session both see content
|
||||
* without overflow. No-op after exit (L4), when no client is attached, or when
|
||||
* the computed dims are unchanged (idempotent).
|
||||
*/
|
||||
function applyMinDims(session: Session): void {
|
||||
if (session.exitedAt !== null || session.clientDims.size === 0) return;
|
||||
let cols = Infinity;
|
||||
let rows = Infinity;
|
||||
for (const d of session.clientDims.values()) {
|
||||
cols = Math.min(cols, d.cols);
|
||||
rows = Math.min(rows, d.rows);
|
||||
}
|
||||
if (!Number.isFinite(cols) || !Number.isFinite(rows)) return;
|
||||
/** Resize the PTY if the dims actually changed and it's still alive (L4). */
|
||||
function applyDims(session: Session, cols: number, rows: number): void {
|
||||
if (session.exitedAt !== null) return;
|
||||
if (session.pty.cols === cols && session.pty.rows === rows) return;
|
||||
session.pty.resize(cols, rows);
|
||||
}
|
||||
@@ -154,10 +142,9 @@ export function createSession(
|
||||
* replay the scrollback to THIS client only so it sees the current screen.
|
||||
* No kicking — devices share the session (invariant #5 relaxed for v0.4).
|
||||
*
|
||||
* The client does NOT get a size vote here: only a client that is actively
|
||||
* viewing the session sends a `resize` (the frontend skips hidden panes), so a
|
||||
* background mirror never clamps the shared PTY. The vote is added on the first
|
||||
* `setClientDims` and removed on `clearClientDims`/`detachWs`.
|
||||
* Attaching does NOT change the PTY size — the device actively using the
|
||||
* session keeps its size. A client only sets the size once it sends a `resize`
|
||||
* (the frontend does so when its pane is visible / regains focus).
|
||||
*/
|
||||
export function attachWs(session: Session, ws: WebSocketLike): void {
|
||||
session.clients.add(ws);
|
||||
@@ -175,10 +162,10 @@ export function attachWs(session: Session, ws: WebSocketLike): void {
|
||||
export function detachWs(session: Session, ws: WebSocketLike, now: number): void {
|
||||
session.clients.delete(ws);
|
||||
session.clientDims.delete(ws);
|
||||
// A client leaving does NOT resize the PTY — whatever device is still actively
|
||||
// viewing keeps its size. Start the idle clock only when the last client goes.
|
||||
if (session.clients.size === 0) {
|
||||
session.detachedAt = now;
|
||||
} else {
|
||||
applyMinDims(session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +179,13 @@ export function writeInput(session: Session, data: string): void {
|
||||
* Record one client's requested dims and resize the PTY to the new min across
|
||||
* all clients (tmux-style). No-op after exit (L4); idempotent when unchanged.
|
||||
*/
|
||||
/**
|
||||
* Latest-writer-wins: the device that most recently fit/focused drives the PTY
|
||||
* size, so whichever device you are actively using is full-screen. (A shared
|
||||
* PTY can only be one size; min-sizing letterboxed the bigger screen.) The
|
||||
* frontend re-sends dims when a pane is shown or its window regains focus, so
|
||||
* switching devices reclaims full size. No-op after exit (L4) / when unchanged.
|
||||
*/
|
||||
export function setClientDims(
|
||||
session: Session,
|
||||
ws: WebSocketLike,
|
||||
@@ -200,16 +194,15 @@ export function setClientDims(
|
||||
): void {
|
||||
if (session.exitedAt !== null) return;
|
||||
session.clientDims.set(ws, { cols, rows });
|
||||
applyMinDims(session);
|
||||
applyDims(session, cols, rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw a client's size vote without detaching it (its tab was hidden). The
|
||||
* client still receives output (it's a background mirror); it just stops
|
||||
* constraining the shared PTY size. Re-votes on its next `setClientDims`.
|
||||
* Forget a client's size when its tab goes hidden — but do NOT resize: the
|
||||
* device still actively viewing keeps its size (no letterboxing of a mirror).
|
||||
*/
|
||||
export function clearClientDims(session: Session, ws: WebSocketLike): void {
|
||||
if (session.clientDims.delete(ws)) applyMinDims(session);
|
||||
session.clientDims.delete(ws);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user