feat(tiles): selections with the grid open focus tiles, and never collapse it by themselves

selectSession gets the tile branch, right after its "already active" early
return: a tiled session is focused in its tile (_selectTiledSession: an
activeSessionId change, the shared _refreshSessionPanels and xterm.focus(),
no cleanup, replay, resize or socket of the parked main terminal). Decision 1:
a USER-initiated pick of a session that is not tiled leaves the grid for the
single view (the grid is remembered), and so does an explicit leaveTiles; an
app-driven pick (auto) never collapses it. A followed #session= link passes
leaveTiles (navigation).

App-driven paths pick a tile instead of the first sessionOrder entry:

- closeSession on the focused tile focuses the neighbouring tile (next in grid
  order, else previous), captured before the await like wasActive, since the
  delete broadcast may already have removed the tile; the last tile closes the
  grid and falls back to the normal pick.
- A tiled session deleted elsewhere loses its tile and a neighbour takes focus
  with auto (the last one lands on the welcome screen as before); a close from
  this tab only drops the tile and leaves the follow-up to closeSession.
- A tiled session popped out to its own window leaves the grid.

Focus rules: pressing a tile is a human selection (pointerdown, never
preventDefault); Ctrl+Tab and Alt+[ / Alt+] cycle through the tiles; only a
human selection acknowledges an idle alert. Home leaves the grid (remembered);
killing every session closes it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-06 16:24:52 +02:00
parent 4341d3dca8
commit 1a04a75c3d
7 changed files with 702 additions and 6 deletions
+55 -1
View File
@@ -110,6 +110,60 @@ describe('selectSession acknowledgement gate', () => {
});
});
describe('with the tile grid open (tile-grid.js)', () => {
// A plain stand-in for the grid: the branch reads only `open` and `has(id)`.
function withGrid(app: ReturnType<typeof makeApp>, tiled: string[]) {
const a = app as Record<string, unknown>;
a._tileGrid = { open: true, has: (id: string) => tiled.includes(id) };
a._selectTiledSession = vi.fn();
a.closeTileGrid = vi.fn();
a._connectWs = vi.fn();
return a;
}
it('a tiled session goes to its tile, never the main terminal, and the tile decides the ack', async () => {
const app = withGrid(makeApp('other'), [SID]);
await (app.selectSession as (id: string) => Promise<void>)(SID);
expect(app._selectTiledSession).toHaveBeenCalledWith(SID, {});
expect(app._cleanupPreviousSession).not.toHaveBeenCalled();
expect(app._connectWs).not.toHaveBeenCalled();
// The branch returns before the main path's own acknowledgement line.
expect(app.markIdleAlertSeen).not.toHaveBeenCalled();
});
it('passes `auto` through, so an app-driven focus stays unacknowledged', async () => {
const app = withGrid(makeApp('other'), [SID]);
await (app.selectSession as (id: string, o: object) => Promise<void>)(SID, { auto: true });
expect(app._selectTiledSession).toHaveBeenCalledWith(SID, { auto: true });
});
it('an `auto` selection of a session that is not tiled leaves the grid open and acknowledges nothing', async () => {
const app = withGrid(makeApp('other'), []);
await (app.selectSession as (id: string, o: object) => Promise<void>)(SID, { auto: true });
expect(app.closeTileGrid).not.toHaveBeenCalled();
expect(app._cleanupPreviousSession).not.toHaveBeenCalled();
expect(app.markIdleAlertSeen).not.toHaveBeenCalled();
});
it('a user-initiated one leaves the grid (remembered) and switches normally, acknowledging', async () => {
const app = withGrid(makeApp('other'), []);
await (app.selectSession as (id: string) => Promise<void>)(SID).catch(() => {});
expect(app.closeTileGrid).toHaveBeenCalledWith({ keepStored: true, reselect: false });
expect(app._cleanupPreviousSession).toHaveBeenCalled();
expect(app.markIdleAlertSeen).toHaveBeenCalledWith(SID);
});
it('`leaveTiles` leaves it too, without acknowledging an `auto` pick', async () => {
const app = withGrid(makeApp('other'), []);
await (app.selectSession as (id: string, o: object) => Promise<void>)(SID, {
auto: true,
leaveTiles: true,
}).catch(() => {});
expect(app.closeTileGrid).toHaveBeenCalledTimes(1);
expect(app.markIdleAlertSeen).not.toHaveBeenCalled();
});
});
describe('the call sites the app drives itself', () => {
// Source guard: these call sites are the reason the flag exists. If a refactor
// moves or reformats them, fail loudly rather than silently going back to
@@ -119,7 +173,7 @@ describe('selectSession acknowledgement gate', () => {
['boot restore, first tab fallback', 'this.selectSession(this.sessionOrder[0], { auto: true });'],
['solo window opening its target', 'this.selectSession(this.soloSessionId, { auto: true });'],
['fallback after the active session is removed', 'this.selectSession(nextSessionId, { auto: true });'],
['a #session=<id> link from another page', 'this.selectSession(id, { auto: true });'],
['a #session=<id> link from another page', 'this.selectSession(id, { auto: true, leaveTiles: true });'],
])('%s passes auto: true', (_label, call) => {
expect(APP_SOURCE).toContain(call);
});