mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 00:49:41 +02:00
Owner decision 10 ("give me then the option to choose only HOW many
tiles, 2,4,6 default is 6 so the menu is easier"; asked where it lives:
"Click opens 6"). The click still opens the grid at once; right-click,
Shift+F10 or the Menu key on the button opens a small menu of three
counts, each drawn as the grid's own layout (2x1, 2x2, 3x2), the
remembered one checked. It replaces the session picker, which is gone
(method, markup hook, CSS, zh-CN entries).
- A pick is remembered per device in codeman:tile-count (default 6;
codeman:tile-grid stays ids only) and opens that many tiles. The click
and Ctrl+Shift+G then open with it, at most what the window fits.
- Which sessions: the rule the click already had (the grid this tab
last had, else an open split's two, else tab order, the active one
included and focused), trimmed from the end (the focused one kept) or
filled from tab order to the count. A remembered grid comes back with
its tiles first, in their cells, holes filled first, then tab order
(owner answer, superseding decision 8's "exactly the stored set"). A
page-load restore still brings back exactly what was stored.
- With the grid open a pick re-forms it: a count change is a shape
change under the cell model's rule (reformTileCells), the focused tile
always kept, every joining tile mounted and laid out before any of
them connects, so each fits once and sends one PTY resize.
- Ctrl/Cmd+click on a tab with the grid closed opens the count in total,
that session among them and focused (owner answer: N, not N+1).
- A count the window cannot fit is greyed out with the reason; a
remembered one stays checked, the keyboard starts on the largest that
fits. Arrows, Home/End, Enter or Space; Escape closes the menu alone
(the global handler gives it the key first, like the tab-group menu)
and puts the keyboard back on the Tiles button; Tab and a click
elsewhere close it.
- zh-CN for every new string (N 个窗格, 窗格数量, the titles, the Help
modal row); the i18n test harvests the menu now, with a session named
"6 tiles" as the user-text trap.
Tests: tile-grid-picker.test.ts becomes tile-grid-count-menu.test.ts
(the button checks kept, every picker check carried over to the menu,
plus keyboard, remembered count, re-form, Ctrl/Cmd+click and the batch
connect); the open-set, cap, restore, shortcuts, split-coexistence and
i18n expectations follow the count; the Help modal test escapes its
label (the new one has parentheses). The vm harness tracks
document.activeElement and makes SVG elements.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
153 lines
5.2 KiB
TypeScript
153 lines
5.2 KiB
TypeScript
/**
|
|
* @fileoverview The tile grid and the split pane are never open together.
|
|
*
|
|
* Both run on TerminalTile, and the split's wrappers (`selectSession`,
|
|
* `_onSessionDeleted` in terminal-split.js) key on `this._splitPane`, so they
|
|
* would fight the grid over the same terminal area if both were ever up:
|
|
*
|
|
* - opening the grid while a split is open closes the split first and seeds
|
|
* the grid with both of its sessions, Pane A focused and Pane B beside it;
|
|
* - while the grid is open, `openSplitPicker` and `openSplitPane` refuse, and
|
|
* the Split button says so (`aria-disabled`, a title);
|
|
* - closing the grid never reopens a split;
|
|
* - the split's wrappers stay inert while the grid is open.
|
|
*
|
|
* Real code: constants.js + app.js + terminal-ui.js + terminal-split.js +
|
|
* tile-grid.js (test/mocks/tile-grid-vm.ts). Port: N/A.
|
|
*/
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
FakeEl,
|
|
FakeTile,
|
|
body,
|
|
bySelector,
|
|
localStore,
|
|
main,
|
|
makeGridApp,
|
|
resetGridHarness,
|
|
type GridApp,
|
|
} from './mocks/tile-grid-vm.js';
|
|
|
|
const IDS = ['s-a', 's-b', 's-c'];
|
|
|
|
function makeSplitButton() {
|
|
const btn = new FakeEl();
|
|
btn.className = 'btn-icon-header btn-split';
|
|
bySelector.set('.btn-split', btn);
|
|
return btn;
|
|
}
|
|
|
|
/** s-a in the main pane, s-b in Pane B (the split's real openSplitPane). */
|
|
function openSplit(app: GridApp) {
|
|
app.openSplitPane('s-b');
|
|
expect(app._splitSessionId).toBe('s-b');
|
|
// closeSplitPane finds its container by selector.
|
|
const container = main.querySelector('.terminal-split-container');
|
|
if (container) bySelector.set('.terminal-split-container', container);
|
|
return FakeTile.all.at(-1) as FakeTile;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
resetGridHarness();
|
|
});
|
|
|
|
describe('opening the grid over an open split', () => {
|
|
it('closes the split and seeds the grid with both of its sessions, Pane A focused', () => {
|
|
const app = makeGridApp(IDS);
|
|
const paneB = openSplit(app);
|
|
|
|
app.toggleTileGrid();
|
|
|
|
expect(paneB.destroy).toHaveBeenCalledTimes(1);
|
|
expect(app._splitPane).toBeNull();
|
|
// The split's two first, then tab order up to the count (default 6).
|
|
expect(app._tileGrid.ids).toEqual(['s-a', 's-b', 's-other', 's-c']);
|
|
expect(app.activeSessionId).toBe('s-a');
|
|
// Pane A is about to park: no closing resize for it.
|
|
expect(app.sendResize).toHaveBeenCalledTimes(1); // the split's own opening resize only
|
|
});
|
|
|
|
it("with a count of 2, exactly the split's two", () => {
|
|
const app = makeGridApp(IDS);
|
|
localStore.set('codeman:tile-count', '2');
|
|
openSplit(app);
|
|
app.toggleTileGrid();
|
|
expect(app._tileGrid.ids).toEqual(['s-a', 's-b']);
|
|
});
|
|
|
|
it('a remembered grid wins over an open split: its tiles first, the split closed and not merged', () => {
|
|
const app = makeGridApp(IDS);
|
|
app.selectSession = vi.fn();
|
|
app.openTileGrid(['s-c']);
|
|
app.closeTileGrid({ reselect: false });
|
|
app.activeSessionId = 's-a';
|
|
localStore.set('codeman:tile-count', '2');
|
|
const paneB = openSplit(app);
|
|
|
|
app.toggleTileGrid();
|
|
|
|
expect(paneB.destroy).toHaveBeenCalledTimes(1);
|
|
expect(app._splitPane).toBeNull();
|
|
// Filled from tab order (s-other first), not seeded with the split's two.
|
|
expect(app._tileGrid.ids).toEqual(['s-c', 's-other']);
|
|
expect(app.activeSessionId).toBe('s-c');
|
|
});
|
|
|
|
it('an explicit open over a split keeps both split sessions first', () => {
|
|
const app = makeGridApp(IDS);
|
|
openSplit(app);
|
|
app.openTileGrid(['s-c']);
|
|
expect(app._tileGrid.ids).toEqual(['s-a', 's-b', 's-c']);
|
|
expect(app.activeSessionId).toBe('s-a');
|
|
});
|
|
});
|
|
|
|
describe('while the grid is open', () => {
|
|
it('the split refuses to open, from the picker or directly', () => {
|
|
const app = makeGridApp(IDS);
|
|
app.openTileGrid(IDS);
|
|
const before = FakeTile.all.length;
|
|
|
|
app.openSplitPicker({ stopPropagation: vi.fn() });
|
|
app.openSplitPane('s-b');
|
|
|
|
expect(app._splitPane ?? null).toBeNull();
|
|
expect(FakeTile.all.length).toBe(before);
|
|
// The picker itself never opened either.
|
|
expect(body.children).toHaveLength(0);
|
|
expect(app._splitPickerDismissHandlers ?? null).toBeNull();
|
|
});
|
|
|
|
it('the Split button is marked unavailable, and back to normal once the grid closes', () => {
|
|
const app = makeGridApp(IDS);
|
|
const btn = makeSplitButton();
|
|
app.openTileGrid(IDS);
|
|
expect(btn.getAttribute('aria-disabled')).toBe('true');
|
|
expect(btn.classList.contains('btn-split--blocked')).toBe(true);
|
|
|
|
app.closeTileGrid({ reselect: false });
|
|
expect(btn.getAttribute('aria-disabled')).toBe('false');
|
|
expect(btn.classList.contains('btn-split--blocked')).toBe(false);
|
|
expect(btn.getAttribute('aria-pressed')).toBe('false');
|
|
});
|
|
|
|
it("the split's wrappers do nothing (no split to close or promote)", () => {
|
|
const app = makeGridApp(IDS);
|
|
app.openTileGrid(IDS);
|
|
app.closeSplitPane = vi.fn();
|
|
app._onSessionDeleted({ id: 's-c' });
|
|
expect(app.closeSplitPane).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('closing the grid', () => {
|
|
it('never reopens the split it replaced', () => {
|
|
const app = makeGridApp(IDS);
|
|
openSplit(app);
|
|
app.toggleTileGrid();
|
|
app.closeTileGrid({ reselect: false });
|
|
expect(app._splitPane).toBeNull();
|
|
expect(app._tilesOwnTerminal()).toBe(false);
|
|
});
|
|
});
|