From 409fd658f28360c1ed40b09aadf234ea77ade308 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Wed, 7 Oct 2026 06:30:09 +0200 Subject: [PATCH] perf(resize): a window resize fits each grid tile once, not twice A window resize reached every grid tile twice: the grid's own ResizeObserver refits them (tile-grid.js _scheduleTileGridRefit, 150 ms trailing), then the main terminal's trailing resize timer (terminal-ui.js throttledResize, 300 ms) ran _forEachTile(fit) over them again. The second pass re-measured six panes and sent nothing (_lastSentDims dedupes the PTY side). The timer now refits the split's Pane B only ({ grid: false }); grid tiles exist only while the grid owns the terminal, and then its observer already covers them. Measured (6 tiles, 20-step window resize and back, headless, tileperf): fit() 12 -> 6 per resize burst; PTY resizes 6 -> 6; browser layouts unchanged within noise (85/75 -> 92/71), so this removes wasted calls only. Live, this code (6 tiles, tileperf): a 20-step window resize, and the resize back, each ran fit() 6 times and sent 6 PTY resizes (12 and 6 before). Test: the timer's one _forEachTile call passes { grid: false } (it lives inside initTerminal, so read from source like the #464 geometry tests); mutation-checked. Scope: PR 2. The line is in terminal-ui.js (a PR 1 seam), but on PR 1 alone _forEachTile reaches only the split's Pane B: the double refit needs the grid. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/web/public/terminal-ui.js | 7 +++++-- test/tile-grid-park-guards.test.ts | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/web/public/terminal-ui.js b/src/web/public/terminal-ui.js index 4dfa7364..17b934cc 100644 --- a/src/web/public/terminal-ui.js +++ b/src/web/public/terminal-ui.js @@ -1428,8 +1428,11 @@ Object.assign(CodemanApp.prototype, { // frame — this observer only ever measured Pane A's container, so // without this call Pane B never learned about a window resize, an // Alt+B sidebar toggle, or a tab-rail drag, and its PTY silently - // stayed at whatever size it was last dragged to. - this._forEachTile?.((tile) => tile.fit()); + // stayed at whatever size it was last dragged to. Grid tiles are left + // out: the grid's own observer (tile-grid.js _scheduleTileGridRefit) + // refits every one of them on the same resize, and a second fit here + // only re-measured six panes to send nothing. + this._forEachTile?.((tile) => tile.fit(), { grid: false }); }, 300); // Trailing-edge: only fire after 300ms of no resize events }; diff --git a/test/tile-grid-park-guards.test.ts b/test/tile-grid-park-guards.test.ts index b995ce60..d7a21b65 100644 --- a/test/tile-grid-park-guards.test.ts +++ b/test/tile-grid-park-guards.test.ts @@ -29,6 +29,8 @@ * tile-grid.js in one `vm` context, with the shared fake DOM and fake * TerminalTile (test/mocks/tile-grid-vm.ts). Port: N/A. */ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { FakeTile, @@ -357,6 +359,22 @@ describe('panes and fonts while the grid is open', () => { expect(pane.sessionId).toBe('s-a'); }); + it("the main terminal's window-resize timer leaves grid tiles to the grid's own observer", () => { + // A window resize fires both: the grid's ResizeObserver refits every tile + // (tile-grid.js _scheduleTileGridRefit) and the main terminal's trailing + // timer used to refit them again, twelve fit() calls for six tiles and + // nothing more sent (measured). The timer lives inside initTerminal(), so + // this reads its source; the split's Pane B is still refitted there. + const src = readFileSync(resolve(import.meta.dirname, '../src/web/public/terminal-ui.js'), 'utf8'); + const start = src.indexOf('const throttledResize = () => {'); + const end = src.indexOf("window.addEventListener('resize', throttledResize)", start); + expect(start).toBeGreaterThan(-1); + expect(end).toBeGreaterThan(start); + const calls = src.slice(start, end).match(/this\._forEachTile\?\.\([^;]*;/g) ?? []; + expect(calls).toHaveLength(1); + expect(calls[0]).toContain('{ grid: false }'); + }); + it('_forEachTile reaches every grid tile; { grid: false } skips them', () => { const app = appWithGrid(true); const seen: string[] = [];