feat(tiles): open and fill tiles by work, then input, then recency

The Tiles button with no stored grid opened the first tabs, so the oldest
sessions got tiles while the busy ones were left out. It now ranks the
open sessions: working first (the most recently started turn first, keyed
off lastSubmitAt only, since a working pane's activity stamp is always
"now"), then the ones needing input (the red and yellow tab alerts), then
everything else by most recent activity, tab order breaking ties.

The ranking is pure (rankTileSessions in constants.js) and reuses the home
screens' classification (_mobileOverviewState) and stamps. It feeds the
toggle's fresh set, a count picked in the menu, Ctrl/Cmd+click and an open
split's fill. The active session is still always included and focused,
detached sessions are never tiled, and a page without the classifier falls
back to tab order. The vm harness now loads mobile-overview.js so the app
tests exercise the real classifier.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-10 05:39:51 +02:00
1 parent 55cd59a346
commit 950450b137
5 files changed
+403 -30

No files matched your search

+9 -8
View File
@@ -3,10 +3,11 @@
*
* - `tileGridOpenSet` (constants.js, pure): what opens, in order: (a) the grid
* this tab last had, if any of its sessions survive; (b) else an open
* split's two sessions; (c) else the open sessions in tab order up to the
* limit (never past the cap of 6), the active one always among them and
* focused. Detached sessions and ones that no longer exist are left out, as
* in the picker.
* split's two sessions; (c) else the open sessions in ranked order (tab
* order when no ranking is given, as here; the ranking itself is pinned in
* test/tile-grid-ranking.test.ts) up to the limit (never past the cap of
* 6), the active one always among them and focused. Detached sessions and
* ones that no longer exist are left out, as in the picker.
* - The button's click and Ctrl+Shift+G are the same function
* (`toggleTileGrid`); right-click (contextmenu) opens the count menu (owner
* decision 10). With the grid open a pick re-forms it, the focused tile kept.
@@ -59,9 +60,9 @@ describe('tileGridOpenSet (what the Tiles button opens)', () => {
});
});
it('c: else the open sessions in tab order up to the limit, the active one focused', () => {
it('c: else the open sessions (no ranking given: tab order) up to the limit, the active one focused', () => {
expect(T.tileGridOpenSet({ ...base, activeId: 't3' })).toEqual({
source: 'tabs',
source: 'ranked',
ids: order.slice(0, 6),
focusedId: 't3',
});
@@ -69,7 +70,7 @@ describe('tileGridOpenSet (what the Tiles button opens)', () => {
it('c: an active session past the limit still comes, with the first ones before it', () => {
expect(T.tileGridOpenSet({ ...base, activeId: 't9' })).toEqual({
source: 'tabs',
source: 'ranked',
ids: ['t1', 't2', 't3', 't4', 't5', 't9'],
focusedId: 't9',
});
@@ -171,7 +172,7 @@ describe('opening at once, in the app', () => {
app.openTileCountMenu({ preventDefault: vi.fn(), stopPropagation: vi.fn() });
expect(app._tilesOwnTerminal()).toBe(true);
pick(4);
// Its two first, then the open sessions in tab order.
// Its two first, then the ranking (every session quiet and unstamped here: tab order).
expect(app._tileGrid.ids).toEqual(['s-a', 's-b', 's-other', 's-c']);
expect(app.activeSessionId).toBe('s-b');
app.openTileCountMenu({ preventDefault: vi.fn(), stopPropagation: vi.fn() });