From ec5e4aa3e16b7eefa610dd365e4e0c0f2d75e421 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Wed, 7 Oct 2026 08:57:42 +0200 Subject: [PATCH] perf(tiles): no snapshot of the session the grid parks when it becomes a tile Opening the grid runs _cleanupPreviousSession() once to park the main terminal, and for a non-shell session that serialized the terminal (1000 lines of scrollback) into the snapshot cache and up to 256 KB of localStorage. Closing the grid drops the main terminal's snapshot of every tiled id (stale by then), so when the parked session is itself a tile, which it is unless the grid opens on a set without it, that copy was always thrown away. openTileGrid now passes skipSnapshot in exactly that case; a parked session that stays out of the grid (Open group as tiles from another session) keeps its snapshot as before. Measured: the same serialize on the main terminal's buffer costs 32 to 42 ms per grid open (n=6, 35 KB) plus the localStorage write; shells never took one, so the A/B runs (shells) show no difference. Snapshot serializes per grid open with a non-shell session active and tiled: 1 -> 0. Tests: the grid passes skipSnapshot only when the parked session is tiled; the real _cleanupPreviousSession skips the serialize only when asked; mutation-checked both ways. Scope: PR 2 (tile-grid.js, the app.js seam). Co-Authored-By: Claude Opus 5.5 (1M context) --- src/web/public/app.js | 5 ++++- src/web/public/tile-grid.js | 6 +++++- test/tile-grid-park-guards.test.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/web/public/app.js b/src/web/public/app.js index 8acdae7e..bcb2cf99 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -7587,7 +7587,9 @@ class CodemanApp { } } - _cleanupPreviousSession(newSessionId) { + // `skipSnapshot`: the tile grid opening on the session it parks + // (tile-grid.js openTileGrid), whose snapshot closing the grid discards. + _cleanupPreviousSession(newSessionId, { skipSnapshot = false } = {}) { // Snapshot the OUTGOING session's xterm rendered state (viewport + scrollback + // colors/attrs) before the terminal gets cleared/reset. Lets us restore the // exact view on switch-back rather than replaying codex's byte stream, which @@ -7598,6 +7600,7 @@ class CodemanApp { // quota for them. Unknown/undefined mode still snapshots, matching restore. const outgoingSession = this.activeSessionId ? this.sessions?.get?.(this.activeSessionId) : null; if ( + !skipSnapshot && this.activeSessionId && outgoingSession?.mode !== 'shell' && this._serializeAddon && diff --git a/src/web/public/tile-grid.js b/src/web/public/tile-grid.js index c8c04f85..84bfc7c8 100644 --- a/src/web/public/tile-grid.js +++ b/src/web/public/tile-grid.js @@ -202,7 +202,11 @@ Object.assign(CodemanApp.prototype, { return true; } - this._cleanupPreviousSession(focus); + // No snapshot of a session that becomes a tile: closing the grid drops the + // main terminal's snapshot of every tiled id (stale by then), so taking one + // here (a 1000-line serialize and up to 256 KB of localStorage for a + // non-shell session) only ever produced a copy that was thrown away. + this._cleanupPreviousSession(focus, { skipSnapshot: wanted.includes(this.activeSessionId) }); grid.open = true; grid.ids = []; grid.focusedId = focus; diff --git a/test/tile-grid-park-guards.test.ts b/test/tile-grid-park-guards.test.ts index d7a21b65..f9565a7f 100644 --- a/test/tile-grid-park-guards.test.ts +++ b/test/tile-grid-park-guards.test.ts @@ -82,6 +82,35 @@ beforeEach(() => { }); describe('parking the main terminal', () => { + it('opening takes no snapshot of a session that becomes a tile, and keeps it for one that does not', () => { + // Closing the grid drops the snapshot of every tiled id, so one taken of a + // tiled session at opening was always thrown away. + const tiled = makeApp(); + tiled.openTileGrid(IDS); + expect(tiled._cleanupPreviousSession).toHaveBeenCalledWith('s-a', { skipSnapshot: true }); + + resetGridHarness(); + const elsewhere = makeApp(); // s-a is active, the grid opens on the others + elsewhere.openTileGrid(['s-b', 's-c']); + expect(elsewhere._cleanupPreviousSession).toHaveBeenCalledWith('s-b', { skipSnapshot: false }); + }); + + it('_cleanupPreviousSession skips the snapshot only when asked', () => { + const app = makeApp(); + delete app._cleanupPreviousSession; // the real one + app._disconnectWs = vi.fn(); + const serialize = vi.fn(() => 'snapshot of s-a\r\n'.repeat(4)); + app._serializeAddon = { serialize }; + app._isUsableXtermSnapshot = () => true; + app._persistXtermSnapshot = vi.fn(); + app._cleanupPreviousSession('s-b', { skipSnapshot: true }); + expect(serialize).not.toHaveBeenCalled(); + expect(app._xtermSnapshots.has('s-a')).toBe(false); + app._cleanupPreviousSession('s-b'); + expect(serialize).toHaveBeenCalledTimes(1); + expect(app._xtermSnapshots.has('s-a')).toBe(true); + }); + it('opening parks it ONCE and puts a tile per session in its place, focused tile active', () => { const app = makeApp(); expect(app.openTileGrid(IDS, { focusedId: 's-b' })).toBe(true);