mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
feat(tiles): no + in the tile header (owner decision 9)
Owner: "remove the + button from these views". The tile header is now ● name ... ⋯ ⤢ ×. Gone with it, because nothing else used them: the + menu (openTileAddMenu, closeTileAddMenu and their hooks in closeTileGrid and the global Escape handler), its "New session in this case" entry and runInCaseForTiles, the .tile-add-empty rules, the four i18n entries only the menu showed, and buildTilePickerSessions' exclude argument (only the menu passed it). Every other way of adding tiles stays and needed nothing from the menu: the Tiles button and its right-click picker, Ctrl/Cmd+click on a tab, dragging a tab onto a tile or an empty slot, "Open group as tiles", and Run joining the open grid (_joinTileGridFromRun). The picker list, the cap helper _tileGridLimit and the user-text skip on names are shared and kept. Tests: the + menu cases (picker, cap, auto-join, the zh-CN harvest) are removed; tile-grid-chrome pins the header as exactly ⋯ ⤢ × with no add menu or runInCaseForTiles left on the app. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,25 +12,13 @@
|
||||
* A tile that joined before its pane existed resends its size once the pid
|
||||
* appears (the server spawned the pane at its own default size).
|
||||
*
|
||||
* A tile's + also offers "New session in this case": the normal Run for the
|
||||
* case the tile's session belongs to, the toolbar's case put back afterwards.
|
||||
*
|
||||
* Real code via the shared vm harness (test/mocks/tile-grid-vm.ts); the Run
|
||||
* hook in session-ui.js is pinned at the source. Port: N/A.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
FakeEl,
|
||||
FakeTile,
|
||||
body,
|
||||
bySelector,
|
||||
makeGridApp,
|
||||
resetGridHarness,
|
||||
section,
|
||||
type GridApp,
|
||||
} from './mocks/tile-grid-vm.js';
|
||||
import { FakeEl, FakeTile, makeGridApp, resetGridHarness, section, type GridApp } from './mocks/tile-grid-vm.js';
|
||||
|
||||
const IDS = ['s-a', 's-b'];
|
||||
const addSession = (app: GridApp, id: string, workingDir = '/w') =>
|
||||
@@ -137,42 +125,3 @@ describe('a pane that starts after its tile connected (#464)', () => {
|
||||
for (const id of IDS) expect(tileOf(id).paneStarted).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('+ / New session in this case', () => {
|
||||
function openMenu(app: GridApp, fromId: string) {
|
||||
app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null }, fromId);
|
||||
const menu = body.children.find((c) => c.className.includes('tile-add-menu'))!;
|
||||
return menu.children.find((c) => c.className === 'tile-add-new')!;
|
||||
}
|
||||
|
||||
it('runs the normal Run in the tile session case, then puts the toolbar case back', async () => {
|
||||
const app = makeGridApp(IDS);
|
||||
app.cases = [{ name: 'proj', path: '/w' }];
|
||||
app._mobileOverviewCaseFor = (dir: string, cases: Array<{ name: string; path: string }>) =>
|
||||
cases.find((c) => dir.startsWith(c.path)) ?? null;
|
||||
const select = new FakeEl();
|
||||
select.value = 'other-case';
|
||||
bySelector.set('#quickStartCase', select);
|
||||
const calls: string[] = [];
|
||||
app.selectQuickStartCase = vi.fn((name: string) => {
|
||||
calls.push(`case:${name}`);
|
||||
select.value = name;
|
||||
});
|
||||
app.run = vi.fn(async () => calls.push(`run in ${select.value}`));
|
||||
app.openTileGrid(IDS);
|
||||
const item = openMenu(app, 's-a');
|
||||
expect(item.disabled).toBe(false);
|
||||
item.dispatch('click');
|
||||
await vi.waitFor(() => expect(calls).toHaveLength(3));
|
||||
expect(calls).toEqual(['case:proj', 'run in proj', 'case:other-case']);
|
||||
expect(app.selectQuickStartCase).toHaveBeenCalledWith('proj', { save: false });
|
||||
});
|
||||
|
||||
it('is disabled for a session that is not in a case', () => {
|
||||
const app = makeGridApp(IDS);
|
||||
app.cases = [];
|
||||
app._mobileOverviewCaseFor = () => null;
|
||||
app.openTileGrid(IDS);
|
||||
expect(openMenu(app, 's-a').disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* the headless frame bar. The layout table still covers 7 to 9 (unreachable).
|
||||
*
|
||||
* Every way into the grid stops at the cap even where the window would fit
|
||||
* nine (the harness window is 2400x1200): opening, adding, a tile's +, a
|
||||
* session Run makes, Ctrl/Cmd+click, the picker and "Open group as tiles".
|
||||
* nine (the harness window is 2400x1200): opening, adding, a session Run
|
||||
* makes, Ctrl/Cmd+click, the picker and "Open group as tiles".
|
||||
* The texts say which limit binds: "at most 6" for the cap, "what this window
|
||||
* fits" for a smaller window.
|
||||
*
|
||||
@@ -63,19 +63,6 @@ describe('the cap', () => {
|
||||
});
|
||||
|
||||
describe('every way in stops at the cap', () => {
|
||||
it("a tile's + offers the rest, disabled, and says why", () => {
|
||||
const app = fullGrid();
|
||||
app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null }, 's-1');
|
||||
const menu = body.children.find((c) => c.className.includes('tile-add-menu'))!;
|
||||
const items = menu.children.filter((c) => ['s-7', 's-8'].includes(c.children[0]?.textContent ?? ''));
|
||||
expect(items).toHaveLength(2);
|
||||
for (const item of items) {
|
||||
expect(item.disabled).toBe(true);
|
||||
expect(item.title).toBe(AT_MOST);
|
||||
}
|
||||
expect(menu.children.find((c) => c.className === 'tile-add-new')!.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('a session Run makes opens on its own, with a toast', () => {
|
||||
const app = fullGrid();
|
||||
expect(app._joinTileGridFromRun('s-7')).toBe(false);
|
||||
|
||||
@@ -97,6 +97,22 @@ describe('the header', () => {
|
||||
});
|
||||
|
||||
describe('header buttons', () => {
|
||||
it('are ⋯ ⤢ × and nothing else: no + (owner decision 9)', () => {
|
||||
const app = openGrid();
|
||||
const tile = section.children.find((el) => el.dataset.sessionId === 's-a') as FakeEl;
|
||||
const actions = tile.children[0].children.find((c) => c.className === 'tile-actions') as FakeEl;
|
||||
expect(actions.children.map((b) => b.className)).toEqual([
|
||||
'tile-btn tile-menu',
|
||||
'tile-btn tile-zoom',
|
||||
'tile-btn tile-remove',
|
||||
]);
|
||||
expect(actions.children.map((b) => b.textContent)).toEqual(['\u22EF', '\u2922', '\u00D7']);
|
||||
// The + menu and its "New session in this case" went with it.
|
||||
for (const gone of ['openTileAddMenu', 'closeTileAddMenu', 'runInCaseForTiles']) {
|
||||
expect(gone in app, gone).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('× removes the tile only: the session keeps running, a neighbour takes focus unacknowledged', () => {
|
||||
const app = openGrid();
|
||||
app._apiDelete = vi.fn();
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*
|
||||
* - Runtime strings are HARVESTED from the real tile code (the shared vm
|
||||
* harness, test/mocks/tile-grid-vm.ts) driven through every state that
|
||||
* writes text: the picker (cap and window wording), a tile's +, the Attach
|
||||
* writes text: the picker (cap and window wording), the Attach
|
||||
* overlay (not attached, attaching, exited, ended), zoom, the header
|
||||
* tooltip, the dividers, the empty slot, every toast, the crash-restart
|
||||
* confirm, the Tiles and Split button titles. Each must translate to text
|
||||
@@ -14,7 +14,7 @@
|
||||
* - Static strings: the shortcut registry's tile entries (overlay and App
|
||||
* Settings list), and index.html run through the real translator in JSDOM
|
||||
* (the Tiles button, the App Settings chips, the Help modal's Tiles rows).
|
||||
* - User text stays as typed: session names (tile header, picker, + menu)
|
||||
* - User text stays as typed: session names (tile header, picker)
|
||||
* and group names carry data-i18n-skip, and a session name inside the
|
||||
* confirm passes through the pattern untranslated.
|
||||
*
|
||||
@@ -130,8 +130,6 @@ async function exercise() {
|
||||
let pill = 'idle';
|
||||
app._sidebarRichRow = () => ({ state: pill, pill, since: { at: 1 } });
|
||||
app._mobileOverviewStampText = () => '3m';
|
||||
app.cases = [{ name: 'proj', path: '/w' }];
|
||||
app._mobileOverviewCaseFor = (_dir: string, cases: Array<{ name: string }>) => cases[0] ?? null;
|
||||
app.sessions.get('s-2').pid = null;
|
||||
app.sessions.get('s-3').paneExit = { status: 3 };
|
||||
app.sessions.get('s-4').paneExit = { signal: 9 };
|
||||
@@ -183,15 +181,6 @@ async function exercise() {
|
||||
harvestAll(app, 'zoomed');
|
||||
app.zoomTile('s-1');
|
||||
|
||||
// A tile's + on a full grid, from a session in a case and from one outside.
|
||||
app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null }, 's-1');
|
||||
harvestAll(app, '+ menu, full');
|
||||
app.closeTileAddMenu();
|
||||
app._mobileOverviewCaseFor = () => null;
|
||||
app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null }, 's-1');
|
||||
harvestAll(app, '+ menu, no case');
|
||||
app.closeTileAddMenu();
|
||||
|
||||
// The toasts of a full grid, by the cap and by the window.
|
||||
app._joinTileGridFromRun('s-7');
|
||||
app.addSessionToTiles('s-7');
|
||||
@@ -264,7 +253,6 @@ describe('every tile grid string the code puts on screen translates to zh-CN', (
|
||||
'Zoom this tile',
|
||||
'Restore the grid',
|
||||
'Session actions',
|
||||
'Add a session to the grid',
|
||||
'Remove tile (the session keeps running)',
|
||||
'idle 3m',
|
||||
'needs you 3m',
|
||||
@@ -276,8 +264,6 @@ describe('every tile grid string the code puts on screen translates to zh-CN', (
|
||||
'The agent exited (signal 9)',
|
||||
'The session ended',
|
||||
'It cannot be restarted in place: close it from ⋯ (Close session).',
|
||||
'New session in this case',
|
||||
'This session is not in a case',
|
||||
'The grid holds at most 6 tiles: the new session opens on its own',
|
||||
'The grid already holds what this window fits (4): the new session opens on its own',
|
||||
'The grid already holds what this window fits (4)',
|
||||
@@ -375,12 +361,12 @@ describe('the static markup through the real translator (JSDOM, zh-CN)', () => {
|
||||
});
|
||||
|
||||
describe('user text in the tile code', () => {
|
||||
it('session names in a tile header, the picker and the + menu are marked data-i18n-skip', () => {
|
||||
it('session names in a tile header and the picker are marked data-i18n-skip', () => {
|
||||
// The harvest saw them only inside skipped subtrees.
|
||||
expect(userText.has('Open tiles')).toBe(true);
|
||||
expect([...seen.keys()]).not.toContain('s-1');
|
||||
const src = read('tile-grid.js');
|
||||
expect(src.match(/setAttribute\('data-i18n-skip', ''\)/g)?.length).toBeGreaterThanOrEqual(3);
|
||||
expect(src.match(/setAttribute\('data-i18n-skip', ''\)/g)?.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('the group names of the grouped rail are marked data-i18n-skip', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @fileoverview The header Tiles button, its picker, and a tile's + menu.
|
||||
* @fileoverview The header Tiles button and its picker.
|
||||
*
|
||||
* - The button is opt-in (`showTileGridButton`, hidden by its `--hidden` marker
|
||||
* class) and hard-gated to desktop widths like Split: a JS width check plus a
|
||||
@@ -12,8 +12,8 @@
|
||||
* opens the grid on the checked ones, focusing the active session if checked.
|
||||
* - Escape closes it, and its close method is idempotent (the global Escape
|
||||
* handler calls every close method).
|
||||
* - A tile's + lists the open sessions not yet tiled; picking one adds and
|
||||
* focuses it; a grid that holds what the window fits disables them.
|
||||
* - A tile has no + (owner decision 9): tiles are added from this picker,
|
||||
* Ctrl/Cmd+click, a dragged tab, a tab group or Run.
|
||||
*
|
||||
* Real code via the shared vm harness (test/mocks/tile-grid-vm.ts). Port: N/A.
|
||||
*/
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
bySelector,
|
||||
makeGridApp,
|
||||
resetGridHarness,
|
||||
section,
|
||||
windowStub,
|
||||
type GridApp,
|
||||
} from './mocks/tile-grid-vm.js';
|
||||
@@ -184,11 +183,10 @@ describe('the picker', () => {
|
||||
expect(() => app.closeTilePicker()).not.toThrow();
|
||||
});
|
||||
|
||||
it('the global Escape handler closes the picker and the + menu', () => {
|
||||
it('the global Escape handler closes the picker', () => {
|
||||
const src = readFileSync(resolve(import.meta.dirname, '../src/web/public/app.js'), 'utf8');
|
||||
const escape = src.slice(src.indexOf("if (e.key === 'Escape') {"), src.indexOf('Option/Alt session navigation'));
|
||||
expect(escape).toContain('this.closeTilePicker?.();');
|
||||
expect(escape).toContain('this.closeTileAddMenu?.();');
|
||||
});
|
||||
|
||||
it('refuses in a narrow window', () => {
|
||||
@@ -198,44 +196,3 @@ describe('the picker', () => {
|
||||
expect(picker()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("a tile's + menu", () => {
|
||||
const addMenu = () => body.children.find((c) => c.className.includes('tile-add-menu')) ?? null;
|
||||
const addButtonOf = (id: string) =>
|
||||
(section.children.find((el) => el.dataset.sessionId === id) as FakeEl).children[0].children[2].children.find((b) =>
|
||||
b.className.includes('tile-add')
|
||||
) as FakeEl;
|
||||
|
||||
it('lists the open sessions not yet tiled; picking one adds and focuses it', () => {
|
||||
const app = makeGridApp(IDS);
|
||||
app.openTileGrid(['s-a', 's-b']);
|
||||
app.markIdleAlertSeen.mockClear();
|
||||
addButtonOf('s-a').dispatch('click', { stopPropagation: vi.fn(), currentTarget: null });
|
||||
const items = addMenu()!.children.filter((i) => i.className !== 'tile-add-new');
|
||||
expect(items.map((i) => i.children[0].textContent)).toEqual(['s-other', 's-c']);
|
||||
items[1].dispatch('click');
|
||||
expect(app._tileGrid.ids).toEqual(['s-a', 's-b', 's-c']);
|
||||
expect(app.activeSessionId).toBe('s-c');
|
||||
expect(app.markIdleAlertSeen).toHaveBeenCalledWith('s-c');
|
||||
expect(addMenu()).toBeNull();
|
||||
});
|
||||
|
||||
it('a grid holding what the window fits disables the entries', () => {
|
||||
const app = makeGridApp(IDS);
|
||||
app.openTileGrid(['s-a', 's-b']);
|
||||
const sectionRect = section.getBoundingClientRect;
|
||||
section.getBoundingClientRect = () => ({ width: 1000, height: 400, top: 0, left: 0, right: 1000, bottom: 400 });
|
||||
app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null });
|
||||
expect(addMenu()!.children.every((i) => i.disabled)).toBe(true);
|
||||
section.getBoundingClientRect = sectionRect;
|
||||
});
|
||||
|
||||
it('closing the grid closes it', () => {
|
||||
const app = makeGridApp(IDS);
|
||||
app.selectSession = vi.fn();
|
||||
app.openTileGrid(['s-a']);
|
||||
app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null });
|
||||
app.closeTileGrid({ reselect: false });
|
||||
expect(addMenu()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user