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:
@@ -208,54 +208,51 @@ describe('attachWs', () => {
|
||||
expect(received(b)).toEqual([{ type: 'output', data: 'live' }]);
|
||||
});
|
||||
|
||||
it('resizes the PTY to the MIN dims across active viewers (tmux-style)', () => {
|
||||
it('attaching alone does NOT change the PTY size (the active device keeps it)', () => {
|
||||
const s = newSession(); // spawn dims 80x24
|
||||
const wide = createMockWs();
|
||||
const narrow = createMockWs();
|
||||
|
||||
// Attach alone does NOT vote on size (a join could be a hidden mirror).
|
||||
attachWs(s, wide);
|
||||
expect(s.pty.cols).toBe(80);
|
||||
|
||||
// A client only constrains the PTY once it reports dims (it's viewing).
|
||||
setClientDims(s, wide, 200, 50);
|
||||
expect(s.pty.cols).toBe(200);
|
||||
|
||||
const hidden = createMockWs();
|
||||
attachWs(s, hidden); // joins but never reports dims (background mirror)
|
||||
// The join must not change the PTY — the active viewer keeps 200x50.
|
||||
expect(s.pty.cols).toBe(200);
|
||||
expect(s.pty.rows).toBe(50);
|
||||
});
|
||||
|
||||
it('latest-writer-wins: the most recent client dims drive the PTY size', () => {
|
||||
const s = newSession();
|
||||
const desktop = createMockWs();
|
||||
const ipad = createMockWs();
|
||||
|
||||
setClientDims(s, desktop, 200, 50);
|
||||
expect(s.pty.cols).toBe(200);
|
||||
expect(s.pty.rows).toBe(50);
|
||||
|
||||
attachWs(s, narrow);
|
||||
setClientDims(s, narrow, 90, 30);
|
||||
// min(200,90)=90, min(50,30)=30
|
||||
// The device used next (smaller iPad) takes over — it is now full-screen.
|
||||
setClientDims(s, ipad, 100, 40);
|
||||
expect(s.pty.cols).toBe(100);
|
||||
expect(s.pty.rows).toBe(40);
|
||||
|
||||
// Switching back to the desktop (re-fit on focus) reclaims its size.
|
||||
setClientDims(s, desktop, 200, 50);
|
||||
expect(s.pty.cols).toBe(200);
|
||||
expect(s.pty.rows).toBe(50);
|
||||
});
|
||||
|
||||
it('clearClientDims (tab hidden) does NOT resize — the active device keeps its size', () => {
|
||||
const s = newSession();
|
||||
const a = createMockWs();
|
||||
const b = createMockWs();
|
||||
setClientDims(s, a, 200, 50);
|
||||
setClientDims(s, b, 90, 30); // b is using it now → 90x30
|
||||
|
||||
clearClientDims(s, a); // a's tab hidden elsewhere → just forget a's dims
|
||||
// PTY unchanged: b is still the active viewer at 90x30.
|
||||
expect(s.pty.cols).toBe(90);
|
||||
expect(s.pty.rows).toBe(30);
|
||||
});
|
||||
|
||||
it('a hidden mirror (no dims reported) does NOT clamp the shared PTY', () => {
|
||||
const s = newSession();
|
||||
const viewer = createMockWs();
|
||||
const hidden = createMockWs();
|
||||
|
||||
attachWs(s, viewer);
|
||||
setClientDims(s, viewer, 200, 50);
|
||||
attachWs(s, hidden); // joins but never reports dims (background tab)
|
||||
|
||||
// The hidden mirror must not drag the PTY down to the 80x24 default.
|
||||
expect(s.pty.cols).toBe(200);
|
||||
expect(s.pty.rows).toBe(50);
|
||||
});
|
||||
|
||||
it('clearClientDims withdraws a vote (tab hidden) and lets the PTY grow', () => {
|
||||
const s = newSession();
|
||||
const wide = createMockWs();
|
||||
const narrow = createMockWs();
|
||||
attachWs(s, wide);
|
||||
setClientDims(s, wide, 200, 50);
|
||||
attachWs(s, narrow);
|
||||
setClientDims(s, narrow, 90, 30); // clamps to 90x30
|
||||
|
||||
clearClientDims(s, narrow); // narrow's tab hidden → withdraw vote
|
||||
expect(s.pty.cols).toBe(200);
|
||||
expect(s.pty.rows).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
// ── detachWs (remove one client) ──────────────────────────────────────────────
|
||||
@@ -280,19 +277,19 @@ describe('detachWs', () => {
|
||||
expect((s.pty as MockIPty).killed).toBe(false);
|
||||
});
|
||||
|
||||
it('lets the PTY grow back when a small client leaves', () => {
|
||||
it('does NOT resize when a client leaves (the remaining device keeps its size)', () => {
|
||||
const s = newSession();
|
||||
const wide = createMockWs();
|
||||
const narrow = createMockWs();
|
||||
attachWs(s, wide);
|
||||
setClientDims(s, wide, 200, 50);
|
||||
attachWs(s, narrow);
|
||||
setClientDims(s, narrow, 90, 30); // clamps to 90x30
|
||||
setClientDims(s, narrow, 90, 30); // narrow is the latest writer → 90x30
|
||||
|
||||
detachWs(s, narrow, 5_000);
|
||||
// only the wide client remains → PTY expands back to 200x50
|
||||
expect(s.pty.cols).toBe(200);
|
||||
expect(s.pty.rows).toBe(50);
|
||||
// detach does not resize; PTY stays at the last set size until a client re-fits.
|
||||
expect(s.pty.cols).toBe(90);
|
||||
expect(s.pty.rows).toBe(30);
|
||||
});
|
||||
|
||||
it('keeps the PTY alive after the last detach: further onData still fills the buffer', () => {
|
||||
|
||||
Reference in New Issue
Block a user