mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-13 10:49:41 +02:00
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>
184 lines
7.6 KiB
TypeScript
184 lines
7.6 KiB
TypeScript
/**
|
|
* @fileoverview The Tiles button opens the grid at once (owner decision 8).
|
|
*
|
|
* - `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 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.
|
|
*
|
|
* The pure helper and the app both via the shared harness (test/mocks/tile-grid-vm.ts).
|
|
* Port: N/A.
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
FakeEl,
|
|
body,
|
|
bySelector,
|
|
documentAddEventListener,
|
|
makeGridApp,
|
|
resetGridHarness,
|
|
windowStub,
|
|
} from './mocks/tile-grid-vm.js';
|
|
|
|
type OpenSet = { source: string; ids: string[]; focusedId: string | null } | null;
|
|
type Helpers = { tileGridOpenSet(p: Record<string, unknown>): OpenSet; TILE_GRID_MAX: number };
|
|
|
|
// constants.js as the harness loaded it (window.CodemanTileGrid).
|
|
const T = windowStub.CodemanTileGrid as Helpers;
|
|
const INDEX_HTML = readFileSync(resolve(import.meta.dirname, '../src/web/public/index.html'), 'utf8');
|
|
|
|
const order = Array.from({ length: 9 }, (_, i) => `t${i + 1}`);
|
|
const sessions = new Map(order.map((id) => [id, { id, name: id }]));
|
|
const base = { sessions, sessionOrder: order, limit: 6 };
|
|
|
|
describe('tileGridOpenSet (what the Tiles button opens)', () => {
|
|
it('a: the grid this tab last had, its focus (or its zoom) included', () => {
|
|
const stored = { ids: ['t4', 't2'], focused: 't2', zoomed: null };
|
|
expect(T.tileGridOpenSet({ ...base, stored, split: ['t1', 't9'], activeId: 't1' })).toEqual({
|
|
source: 'stored',
|
|
ids: ['t4', 't2'],
|
|
focusedId: 't2',
|
|
});
|
|
expect(T.tileGridOpenSet({ ...base, stored: { ids: ['t4', 't2'], focused: 't4', zoomed: 't2' } })?.focusedId).toBe(
|
|
't2'
|
|
);
|
|
});
|
|
|
|
it('b: else an open split, Pane A focused', () => {
|
|
expect(T.tileGridOpenSet({ ...base, stored: { ids: [] }, split: ['t5', 't7'], activeId: 't5' })).toEqual({
|
|
source: 'split',
|
|
ids: ['t5', 't7'],
|
|
focusedId: 't5',
|
|
});
|
|
});
|
|
|
|
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: 'ranked',
|
|
ids: order.slice(0, 6),
|
|
focusedId: 't3',
|
|
});
|
|
});
|
|
|
|
it('c: an active session past the limit still comes, with the first ones before it', () => {
|
|
expect(T.tileGridOpenSet({ ...base, activeId: 't9' })).toEqual({
|
|
source: 'ranked',
|
|
ids: ['t1', 't2', 't3', 't4', 't5', 't9'],
|
|
focusedId: 't9',
|
|
});
|
|
});
|
|
|
|
it('c: a window that takes fewer gets fewer; nothing ever passes the cap', () => {
|
|
expect(T.tileGridOpenSet({ ...base, limit: 4, activeId: 't1' })?.ids).toEqual(['t1', 't2', 't3', 't4']);
|
|
expect(T.tileGridOpenSet({ ...base, limit: 99, activeId: 't1' })?.ids).toHaveLength(T.TILE_GRID_MAX);
|
|
});
|
|
|
|
it('leaves out detached sessions and ones that no longer exist, as the picker does', () => {
|
|
const detachedIds = new Set(['t2']);
|
|
const out = T.tileGridOpenSet({ ...base, sessionOrder: ['gone', ...order], detachedIds, activeId: 't1' });
|
|
expect(out?.ids).toEqual(['t1', 't3', 't4', 't5', 't6', 't7']);
|
|
// A split whose Pane B was popped out: Pane A alone.
|
|
expect(T.tileGridOpenSet({ ...base, split: ['t1', 't2'], detachedIds, activeId: 't1' })?.ids).toEqual(['t1']);
|
|
});
|
|
|
|
it('is null when there is nothing to open', () => {
|
|
expect(T.tileGridOpenSet({ sessions: new Map(), sessionOrder: [], limit: 6, activeId: null })).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('the Tiles button and Ctrl+Shift+G', () => {
|
|
const button = () => {
|
|
const html = INDEX_HTML.match(/<button class="btn-icon-header btn-tile-grid[^>]*>/)?.[0] ?? '';
|
|
return html;
|
|
};
|
|
|
|
it('a click opens the grid at once (the toggle), a right-click opens the count menu', () => {
|
|
expect(button()).toContain('onclick="app.toggleTileGrid()"');
|
|
expect(button()).toContain('oncontextmenu="app.openTileCountMenu(event)"');
|
|
expect(button()).toContain('right-click for how many');
|
|
expect(button()).not.toContain('onclick="app.openTileCountMenu');
|
|
});
|
|
|
|
it('Ctrl+Shift+G runs the same toggle', () => {
|
|
const src = readFileSync(resolve(import.meta.dirname, '../src/web/public/tile-grid.js'), 'utf8');
|
|
expect(src).toContain("if (id === 'toggle-tile-grid') this.toggleTileGrid();");
|
|
});
|
|
});
|
|
|
|
describe('opening at once, in the app', () => {
|
|
const IDS = ['s-a', 's-b', 's-c'];
|
|
const picker = () => body.children.find((c) => c.id === 'tileCountMenu') ?? null;
|
|
const pick = (n: number) =>
|
|
picker()!
|
|
.children.find((c) => c.dataset.count === String(n))!
|
|
.dispatch('click', { stopPropagation: vi.fn() });
|
|
beforeEach(() => {
|
|
resetGridHarness();
|
|
const wrap = new FakeEl();
|
|
bySelector.set('.terminal-wrap', wrap);
|
|
});
|
|
|
|
it('a click with the grid closed shows the tiles, no menu', () => {
|
|
const app = makeGridApp(IDS);
|
|
app.activeSessionId = 's-b';
|
|
app.toggleTileGrid();
|
|
expect(picker()).toBeNull();
|
|
expect(app._tilesOwnTerminal()).toBe(true);
|
|
expect(app._tileGrid.ids).toEqual(['s-other', ...IDS]);
|
|
expect(app.activeSessionId).toBe('s-b');
|
|
});
|
|
|
|
it('a click closes a menu that a right-click left open', () => {
|
|
const app = makeGridApp(IDS);
|
|
app.openTileCountMenu({ preventDefault: vi.fn(), stopPropagation: vi.fn() });
|
|
expect(picker()).not.toBeNull();
|
|
app.toggleTileGrid();
|
|
expect(picker()).toBeNull();
|
|
expect(app._tilesOwnTerminal()).toBe(true);
|
|
});
|
|
|
|
it('a click elsewhere closes the menu; a click inside it does not', () => {
|
|
const app = makeGridApp(IDS);
|
|
const before = documentAddEventListener.mock.calls.length;
|
|
app.openTileCountMenu({ preventDefault: vi.fn() });
|
|
const calls = documentAddEventListener.mock.calls.slice(before) as Array<[string, (e: unknown) => void]>;
|
|
const onClick = calls.find(([type]) => type === 'click')![1];
|
|
onClick({ target: picker()!.children[0] });
|
|
expect(picker()).not.toBeNull();
|
|
onClick({ target: new FakeEl() });
|
|
expect(picker()).toBeNull();
|
|
});
|
|
|
|
it('a right-click opens the count menu and keeps the browser menu away', () => {
|
|
const app = makeGridApp(IDS);
|
|
const ev = { preventDefault: vi.fn(), stopPropagation: vi.fn() };
|
|
app.openTileCountMenu(ev);
|
|
expect(ev.preventDefault).toHaveBeenCalled();
|
|
expect(picker()).not.toBeNull();
|
|
expect(app._tilesOwnTerminal()).toBe(false);
|
|
});
|
|
|
|
it('a right-click with the grid open re-forms it to the count picked, the focused tile kept', () => {
|
|
const app = makeGridApp(IDS);
|
|
app.openTileGrid(['s-a', 's-b'], { focusedId: 's-b' });
|
|
app.openTileCountMenu({ preventDefault: vi.fn(), stopPropagation: vi.fn() });
|
|
expect(app._tilesOwnTerminal()).toBe(true);
|
|
pick(4);
|
|
// 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() });
|
|
pick(2);
|
|
expect(app._tileGrid.ids).toEqual(['s-a', 's-b']);
|
|
expect(app.activeSessionId).toBe('s-b');
|
|
});
|
|
});
|