docs(tiles): a reload's fill ranks without pending approvals, by design

A page reload puts a stored open grid back inside handleInit and fills a
cell freed since (its session deleted while the grid was closed) from the
ranking. Pending approvals, the source of the needs-input group, reach the
page only through seedApprovals, an async GET that lands after the restore,
so for that one fill a session waiting on a permission dialog or an unseen
finished turn ranks with the quiet ones (working sessions still rank first).

Holding the fill back until the seed lands would open fewer tiles, which
can be another shape, and then reshape the grid and move the user's tiles a
moment after the reload, so a late seed never re-forms a restored grid. The
plan, architecture-invariants#tile-grid and the wiki now say so, and a test
pins it: the seed arrives after the restore, the ranking then puts the
needs-input session first, and the restored cells and the stored layout
stay as they came back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-10 06:31:26 +02:00
parent 7b95856cc5
commit fb5c1bfca5
4 changed files with 67 additions and 6 deletions
+50 -2
View File
@@ -12,8 +12,9 @@
* `selectSession(restoreId, { auto: true })`: with a stored open grid the
* main terminal never loads on that page load (no select, no socket, no
* capture), a deleted or detached session frees its cell for the ranking
* to fill, duplicate ids are dropped, and the stored fractions and zoom
* come back. A narrow window keeps the single view.
* to fill (on status and stamps: pending approvals are seeded after, and
* never re-form the restored grid), duplicate ids are dropped, and the
* stored fractions and zoom come back. A narrow window keeps the single view.
* - A `#session=` link on load wins, and leaves the stored grid remembered
* but closed.
* - Leaving the grid invalidates the main terminal's cached content for every
@@ -174,6 +175,53 @@ describe('page load with a stored open grid', () => {
expect(app._tileGrid.cells).toEqual(['s-a', 's-d', 's-b']);
});
it('pending approvals seeded after the reload never re-form the restored grid (known limit of the reload fill)', async () => {
// seedApprovals is async (GET /api/approvals), so the freed cell is filled
// from status and stamps alone. A needs-input session the seed reveals a
// moment later does not take a tile from the grid already back on screen:
// a late fill would reshape it (fewer tiles opened is another shape) and
// move the user's tiles a second after the reload.
storeGrid({ ids: ['s-a', 'gone', 's-b'], focused: 's-a' });
const now = 1_000_000;
let release!: () => void;
const app = pageLoad(
IDS,
(a) => {
a.tabAlerts = new Map();
a.seedApprovals = vi.fn(
() =>
new Promise<void>((resolve) => {
release = () => {
a.setPendingHook('s-d', 'permission_prompt');
resolve();
};
})
);
},
{
sessions: [
{ id: 's-a', name: 's-a', mode: 'claude', pid: 1, status: 'idle', lastActivityAt: now },
{ id: 's-b', name: 's-b', mode: 'claude', pid: 1, status: 'idle', lastActivityAt: now },
{ id: 's-c', name: 's-c', mode: 'claude', pid: 1, status: 'idle', lastActivityAt: now - 10 },
{ id: 's-d', name: 's-d', mode: 'claude', pid: 1, status: 'idle', lastActivityAt: now - 500 },
],
}
);
expect(app.seedApprovals).toHaveBeenCalledTimes(1);
// Before the seed lands: all quiet, the most recent (s-c) takes the freed cell.
expect(app._tileGrid.cells).toEqual(['s-a', 's-c', 's-b']);
const before = stored();
release();
await Promise.resolve();
await Promise.resolve();
// The seed made s-d need input: the ranking now puts it first...
expect(app._tileGridRanking()[0]).toBe('s-d');
app._renderTileChrome();
// ...but the restored grid and the stored layout stay as they came back.
expect(app._tileGrid.cells).toEqual(['s-a', 's-c', 's-b']);
expect(stored()).toEqual(before);
});
it('brings back the fractions (same layout only) and a zoom the user chose', () => {
storeGrid({ ids: IDS, focused: 's-a', zoomed: 's-b', colFr: [2, 1], rowFr: [1, 1, 1] });
const app = pageLoad(IDS);