revert(session): drop the cross-device needsRefresh buffer reload

The post-takeover/re-assert needsRefresh made multi-client redraws worse
in practice (fragmented mixed-width frames on the phone) — reverted to
the behavior the user verified as good: cross-device reflows rely on
Ink's own redraw, stale scrollback scrolls away with new output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-06-10 20:50:24 +02:00
co-authored by Claude Fable 5
parent 68310619a7
commit 28b531fa5b
2 changed files with 1 additions and 73 deletions
+1 -37
View File
@@ -1721,12 +1721,6 @@ export class Session extends EventEmitter {
this.activityTimeout = null; this.activityTimeout = null;
} }
// Clear pending cross-device resize refresh
if (this._resizeRefreshTimer) {
clearTimeout(this._resizeRefreshTimer);
this._resizeRefreshTimer = null;
}
// Clear line buffer flush timer // Clear line buffer flush timer
if (this._lineBufferFlushTimer) { if (this._lineBufferFlushTimer) {
clearTimeout(this._lineBufferFlushTimer); clearTimeout(this._lineBufferFlushTimer);
@@ -2167,25 +2161,6 @@ export class Session extends EventEmitter {
/** True while a small viewport reflowed the pane past an idle desktop claim. */ /** True while a small viewport reflowed the pane past an idle desktop claim. */
private _mobileSizeOverride = false; private _mobileSizeOverride = false;
/** Debounce for the post-takeover buffer refresh (see _scheduleResizeRefresh) */
private _resizeRefreshTimer: NodeJS.Timeout | null = null;
/**
* After a CROSS-DEVICE resize (phone takes the pane / desktop re-asserts),
* viewing clients still hold the old-width buffer: Ink's redraw lands below
* the stale frames, stacking ghost footers. Tell every client to reload the
* buffer once the post-SIGWINCH redraw has settled. Debounced so a takeover
* followed by an immediate re-assert produces a single refresh.
*/
private _scheduleResizeRefresh(): void {
if (this._resizeRefreshTimer) clearTimeout(this._resizeRefreshTimer);
this._resizeRefreshTimer = setTimeout(() => {
this._resizeRefreshTimer = null;
if (this._isStopped) return;
this.emit('needsRefresh');
}, 700);
}
/** Register a live desktop sizing claim (see _desktopSizeClaims). */ /** Register a live desktop sizing claim (see _desktopSizeClaims). */
claimDesktopSizing(token: symbol): void { claimDesktopSizing(token: symbol): void {
this._desktopSizeClaims.add(token); this._desktopSizeClaims.add(token);
@@ -2205,8 +2180,7 @@ export class Session extends EventEmitter {
noteDesktopActivity(): void { noteDesktopActivity(): void {
this._lastDesktopActivityAt = Date.now(); this._lastDesktopActivityAt = Date.now();
if (this._mobileSizeOverride && this._lastDesktopDims) { if (this._mobileSizeOverride && this._lastDesktopDims) {
// resize()'s desktop branch clears _mobileSizeOverride — leaving it set this._mobileSizeOverride = false;
// here lets resize() recognize the re-assert and refresh the clients.
this.resize(this._lastDesktopDims.cols, this._lastDesktopDims.rows, { viewportType: 'desktop' }); this.resize(this._lastDesktopDims.cols, this._lastDesktopDims.rows, { viewportType: 'desktop' });
} }
} }
@@ -2229,10 +2203,6 @@ export class Session extends EventEmitter {
*/ */
resize(cols: number, rows: number, options: { viewportType?: ResizeViewportType } = {}): void { resize(cols: number, rows: number, options: { viewportType?: ResizeViewportType } = {}): void {
const isSmallViewport = options.viewportType === 'mobile' || options.viewportType === 'tablet'; const isSmallViewport = options.viewportType === 'mobile' || options.viewportType === 'tablet';
// Cross-device transitions (detected before the flags are updated below):
// a desktop resize arriving while a mobile override is active = re-assert.
const reasserting = options.viewportType === 'desktop' && this._mobileSizeOverride;
let tookOver = false;
if (options.viewportType === 'desktop') { if (options.viewportType === 'desktop') {
this._lastDesktopDims = { cols, rows }; this._lastDesktopDims = { cols, rows };
this._lastDesktopActivityAt = Date.now(); this._lastDesktopActivityAt = Date.now();
@@ -2242,7 +2212,6 @@ export class Session extends EventEmitter {
if (Date.now() - this._lastDesktopActivityAt < Session.DESKTOP_CLAIM_IDLE_MS) { if (Date.now() - this._lastDesktopActivityAt < Session.DESKTOP_CLAIM_IDLE_MS) {
return; return;
} }
tookOver = !this._mobileSizeOverride;
this._mobileSizeOverride = true; this._mobileSizeOverride = true;
} }
if (this.ptyProcess && (cols !== this._ptyCols || rows !== this._ptyRows)) { if (this.ptyProcess && (cols !== this._ptyCols || rows !== this._ptyRows)) {
@@ -2252,11 +2221,6 @@ export class Session extends EventEmitter {
this._mux.resizeWindow?.(this._muxSession.muxName, cols, rows); this._mux.resizeWindow?.(this._muxSession.muxName, cols, rows);
} }
this.ptyProcess.resize(cols, rows); this.ptyProcess.resize(cols, rows);
// Cross-device reflow: all clients reload the buffer so stale-width
// frames don't stack above the fresh Ink redraw (ghost footers).
if (tookOver || reasserting) {
this._scheduleResizeRefresh();
}
} }
} }
-36
View File
@@ -171,41 +171,5 @@ describe('Session resize arbitration', () => {
session.noteDesktopActivity(); session.noteDesktopActivity();
expect(resize).not.toHaveBeenCalled(); expect(resize).not.toHaveBeenCalled();
}); });
it('emits needsRefresh after a mobile takeover and after a desktop re-assert', () => {
vi.useFakeTimers();
const session = new Session({ workingDir: '/tmp', mode: 'shell' });
attachFakePty(session, 160, 48);
const refresh = vi.fn();
session.on('needsRefresh', refresh);
session.resize(208, 45, { viewportType: 'desktop' });
session.claimDesktopSizing(Symbol('desktop-conn'));
vi.advanceTimersByTime(800);
expect(refresh).not.toHaveBeenCalled(); // plain desktop resize: no refresh
vi.advanceTimersByTime(PAST_IDLE_MS);
session.resize(48, 28, { viewportType: 'mobile' }); // takeover
vi.advanceTimersByTime(800);
expect(refresh).toHaveBeenCalledTimes(1);
session.noteDesktopActivity(); // re-assert to 208x45
vi.advanceTimersByTime(800);
expect(refresh).toHaveBeenCalledTimes(2);
});
it('does not emit needsRefresh for ordinary single-device resizes', () => {
vi.useFakeTimers();
const session = new Session({ workingDir: '/tmp', mode: 'shell' });
attachFakePty(session, 160, 48);
const refresh = vi.fn();
session.on('needsRefresh', refresh);
session.resize(48, 28, { viewportType: 'mobile' }); // mobile-only, no claims
session.resize(208, 45, { viewportType: 'desktop' });
session.resize(100, 30); // untyped
vi.advanceTimersByTime(1000);
expect(refresh).not.toHaveBeenCalled();
});
}); });
}); });