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);