From 3975089af37e8f626e9f6a35eab14985d9f32487 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Tue, 13 Oct 2026 18:58:22 +0200 Subject: [PATCH] fix(tiles): a tile docked in another window leaves this window's stored grid alone (#584 merge-time fix) There is one stored grid per browser (codeman:tile-grid), and the window that docked the tile has just written it. When this window then hears the claim and drops the tile, removeTile (or, for its last tile, closeTileGrid with keepStored: false) overwrote or forgot that stored grid. The removal now runs under _tilePersistHold, and the last-tile close keeps the stored grid. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/web/public/tile-grid.js | 12 ++++++++++-- test/tile-grid-detach.test.ts | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/web/public/tile-grid.js b/src/web/public/tile-grid.js index fca591c2..35ab7224 100644 --- a/src/web/public/tile-grid.js +++ b/src/web/public/tile-grid.js @@ -3012,8 +3012,16 @@ Object.assign(CodemanApp.prototype, { this._lastTileDrag = null; const grid = this._tileGrid; if (!grid?.has(sessionId)) return false; - if (grid.ids.length > 1) return this.removeTile(sessionId, { focus: false }); - this.closeTileGrid({ keepStored: false, reselect: false }); + // One stored grid per browser, and the window that docked the tile has + // just written it: this window losing the tile is no change of the + // user's to remember over that one (nor to forget it). + this._tilePersistHold = true; + try { + if (grid.ids.length > 1) return this.removeTile(sessionId, { focus: false }); + this.closeTileGrid({ keepStored: true, reselect: false }); + } finally { + this._tilePersistHold = false; + } this.activeSessionId = null; try { localStorage.removeItem('codeman-active-session'); diff --git a/test/tile-grid-detach.test.ts b/test/tile-grid-detach.test.ts index fdc7d637..d8b6205c 100644 --- a/test/tile-grid-detach.test.ts +++ b/test/tile-grid-detach.test.ts @@ -42,6 +42,7 @@ import { FakeTile, documentAddEventListener, documentRemoveEventListener, + localStore, makeGridApp, pointHits, resetGridHarness, @@ -281,6 +282,40 @@ describe('another window docking the tile this one dragged', () => { expect(app.terminal.clear).toHaveBeenCalled(); }); + // One stored grid per browser: the window that docked the tile wrote it a + // moment before this one hears the claim. Losing the tile here is not a + // layout of this window's to store over that one, nor a reason to forget it. + const docked = JSON.stringify({ + v: 1, + open: true, + ids: ['s-x', 's-b'], + count: 2, + focused: 's-b', + zoomed: null, + colFr: [1, 1], + rowFr: [1], + }); + + it("the stored grid stays the docking window's when this one keeps other tiles", () => { + const app = openGrid(FOUR); + startDrag('s-b'); + endDrag('s-b', { clientX: -50, clientY: 300 }, 'move'); + localStore.set('codeman:tile-grid', docked); + app._onWindowMessage({ type: 'tile-adopted', id: 's-b', by: 'c-there' }); + expect(app._tileGrid.ids).toEqual(['s-a', 's-c', 's-d']); + expect(localStore.get('codeman:tile-grid')).toBe(docked); + }); + + it("the stored grid stays the docking window's when this one's last tile leaves", () => { + const app = openGrid(['s-b']); + startDrag('s-b'); + endDrag('s-b', { clientX: -50, clientY: 300 }, 'move'); + localStore.set('codeman:tile-grid', docked); + app._onWindowMessage({ type: 'tile-adopted', id: 's-b', by: 'c-there' }); + expect(app._tileGrid.open).toBe(false); + expect(localStore.get('codeman:tile-grid')).toBe(docked); + }); + it('a claim for a tile this window did not drag, or one it dragged long ago, is ignored', () => { const app = openGrid(FOUR); app._onWindowMessage({ type: 'tile-adopted', id: 's-b', by: 'c-there' });