fix(tiles): Redraw on a tile sends the forced resize and reports only what it sent

Redraw (Ctrl+Shift+R and the header button, restoreTerminalSize) on a
focused tile or the split's Pane B called tile.fit({ force: true }) and
always toasted "Terminal restored to CxR". In TerminalTile._sendResize,
force only skipped the client-side dedupe: the frame carried no `f`, so
Session.resize skipped a size equal to the one it last applied and the
server did nothing. And _sendResize returned silently with the socket down
or the session popped out to its own window, while the toast still
claimed success.

Both halves are fixed, the first as parity with the primary pane:
- a forced fit now sends `f: true`, the flag the primary's sendResize sets,
  which the server honours (ws-routes reads msg.f, Session.resize then runs
  tmux resize-window and the PTY resize at the same size);
- fit() and _sendResize() return whether a frame went out, and
  restoreTerminalSize toasts success only then. Otherwise it says why, as
  the primary branch does: "sized by its own window" for a detached
  session, "not connected" while the tile's socket is down (it announces
  its size again on reopen), and the primary's "Could not determine
  terminal size" for a pane that measured nothing.

What this does not claim: a forced resize to the size the PTY already has
changes no geometry, so it is not a cure for a garbled tile whose PTY
already matches; the primary pane's forced resize has the same limit. It
matters when the server's recorded size has drifted from the tmux window.

Tests: the forced frame carries f:true (and plain ones do not), fit()'s
return value on send, dedupe, detached and closed-socket paths, and Redraw
end to end on a real tile (sent, socket down, popped out), plus the three
no-success toasts in focused-pane-shortcuts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-09 09:20:19 +02:00
parent eb5d982c38
commit cdb3c34ae0
5 changed files with 138 additions and 16 deletions
+30
View File
@@ -127,6 +127,36 @@ describe('terminal shortcuts follow the focused pane', () => {
expect(app.showToast).toHaveBeenCalledWith('Terminal restored to 60x30', 'success');
});
// TerminalTile.fit() returns whether its resize went out. When it did not,
// Redraw used to report a size that was never sent.
it.each([
[
'popped out to its own window',
{ detached: true, wsReady: true },
'This session is sized by its own window',
'warning',
],
[
'whose socket is down',
{ detached: false, wsReady: false },
'Terminal not connected: its size is sent when it reconnects',
'warning',
],
['that could not measure itself', { detached: false, wsReady: true }, 'Could not determine terminal size', 'error'],
])('Ctrl+Shift+R on a second pane %s reports no success', async (_label, state, message, level) => {
const app = loadApp();
app.detachedSessions = new Set(state.detached ? ['session-b'] : []);
const tile = paneB({ fit: vi.fn(() => false), _wsReady: state.wsReady });
app._noteFocusedTile(tile);
await app.restoreTerminalSize();
expect(tile.fit).toHaveBeenCalledWith({ force: true });
expect(app.showToast).toHaveBeenCalledTimes(1);
expect(app.showToast).toHaveBeenCalledWith(message, level);
expect(app.sendResize).not.toHaveBeenCalled();
});
it('Ctrl+Shift+R keeps restoring the primary when it holds the keyboard', async () => {
const app = loadApp();
+70 -5
View File
@@ -118,7 +118,7 @@ type Tile = {
connect(): Promise<void>;
destroy(): void;
reconnectNow(): void;
fit(opts?: { force?: boolean }): void;
fit(opts?: { force?: boolean }): boolean;
detachedSessions?: Set<string>;
ws: FakeSocket | null;
_reconnectAttempts: number;
@@ -514,15 +514,80 @@ describe('TerminalTile geometry (#464: the pane and its PTY never disagree)', ()
const { tile, ws } = await connectTile(makeApp());
ws.open();
tile.fit();
expect(tile.fit()).toBe(false); // unchanged: nothing sent
expect(resizeFrames(ws)).toHaveLength(1);
FakeFit.proposed = { cols: 100, rows: 30 };
tile.fit();
expect(tile.fit()).toBe(true);
expect(resizeFrames(ws).at(-1)).toEqual({ t: 'z', c: 100, r: 30, v: 'desktop' });
tile.fit({ force: true });
expect(tile.fit({ force: true })).toBe(true);
expect(resizeFrames(ws)).toHaveLength(3);
// The primary pane's forced resize flag: without it Session.resize skips a
// size equal to the one it last applied, and the forced resend did nothing.
expect(resizeFrames(ws).at(-1)).toEqual({ t: 'z', c: 100, r: 30, v: 'desktop', f: true });
});
it('force on a closed socket sends nothing and says so', async () => {
const { tile, ws } = await connectTile(makeApp());
ws.open();
ws.drop(1006);
expect(tile.fit({ force: true })).toBe(false);
expect(resizeFrames(ws)).toHaveLength(1); // only the open's own announcement
});
describe('Redraw (Ctrl+Shift+R, the header button) on a focused tile', () => {
type RedrawApp = App & {
restoreTerminalSize(): Promise<void>;
_noteFocusedTile(tile: unknown): void;
detachedSessions?: Set<string>;
};
it('forces the resize through to the server and reports the size it sent', async () => {
const app = makeApp() as RedrawApp;
const { tile, ws } = await connectTile(app);
ws.open();
app._noteFocusedTile(tile);
await app.restoreTerminalSize();
expect(resizeFrames(ws).at(-1)).toEqual({ t: 'z', c: 80, r: 24, v: 'desktop', f: true });
expect(app.showToast).toHaveBeenCalledWith('Terminal restored to 80x24', 'success');
});
it('with the socket down, sends nothing and does not report a success', async () => {
const app = makeApp() as RedrawApp;
const { tile, ws } = await connectTile(app);
ws.open();
app._noteFocusedTile(tile);
ws.drop(1006);
await app.restoreTerminalSize();
expect(resizeFrames(ws)).toHaveLength(1);
expect(app.showToast).not.toHaveBeenCalledWith(expect.stringContaining('restored'), 'success');
expect(app.showToast).toHaveBeenCalledWith(
'Terminal not connected: its size is sent when it reconnects',
'warning'
);
});
it('for a session popped out to its own window, says that window sizes it', async () => {
const app = makeApp() as RedrawApp;
const detached = new Set<string>();
app.detachedSessions = detached;
const { tile, ws } = await connectTile(app, { detachedSessions: detached });
ws.open();
app._noteFocusedTile(tile);
detached.add('s-tile'); // popped out after the tile opened
await app.restoreTerminalSize();
expect(resizeFrames(ws)).toHaveLength(1);
expect(app.showToast).not.toHaveBeenCalledWith(expect.stringContaining('restored'), 'success');
expect(app.showToast).toHaveBeenCalledWith('This session is sized by its own window', 'warning');
});
});
it('re-announces an unchanged size on a reconnected socket', async () => {
@@ -598,7 +663,7 @@ describe('TerminalTile geometry (#464: the pane and its PTY never disagree)', ()
ws.open();
FakeFit.proposed = { cols: 120, rows: 40 };
tile.fit();
expect(tile.fit()).toBe(false);
expect(resizeFrames(ws)).toEqual([]);
});