mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
feat(tiles): the grid holds at most 6 tiles (owner decision 7)
Six was tested smooth on the owner's desktop; nine missed the headless frame bar and is untested on real hardware. TILE_GRID_MAX (constants.js) is now 6 and stays the one cap every limit reads; the layout table gets its own bound, TILE_LAYOUT_MAX = 9, so the 7 to 9 layouts keep working (unreachable) and going back to nine is that one line. Every way in stops at the cap: opening, addTile, a tile's +, a session Run makes, Ctrl/Cmd+click, the picker, "Open group as tiles", and a stored grid with more ids (it comes back as its first six, focus kept only if it survives, a dropped zoom cleared, row fractions that no longer match the 3x2 reset). The limits now go through one helper, _tileGridLimit(), whose texts say which limit binds: "Up to 6 tiles" / "The grid holds at most 6 tiles" when it is the cap, "This window fits N" when it is the window. Docs: decision 7 and an as-built entry in the spec, CLAUDE.md, the invariants, the wiki's Tile Grid and Dashboard pages. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1636,11 +1636,19 @@ function buildSplitPickerSessions(sessions, sessionOrder, excludeId, detachedIds
|
||||
// ── Tile grid (tile-grid.js) ───────────────────────────────────────────────
|
||||
//
|
||||
// Pure layout and state helpers for the tile grid (docs/tile-grid-plan.md):
|
||||
// 1 to 9 live sessions side by side, each in its own TerminalTile. Desktop
|
||||
// only, behind the same 1180px gate as the split pane.
|
||||
// 1 to TILE_GRID_MAX live sessions side by side, each in its own TerminalTile.
|
||||
// Desktop only, behind the same 1180px gate as the split pane.
|
||||
|
||||
/** Hard cap on tiles in one grid. */
|
||||
const TILE_GRID_MAX = 9;
|
||||
/**
|
||||
* Hard cap on tiles in one grid: the ONE place it is set (owner decision 7 in
|
||||
* docs/tile-grid-plan.md). Six was tested smooth on a real desktop; nine missed
|
||||
* the headless frame bar and is untested on hardware. Everything that limits
|
||||
* the grid reads this, and the layout table still covers up to TILE_LAYOUT_MAX,
|
||||
* so raising the cap is this one line.
|
||||
*/
|
||||
const TILE_GRID_MAX = 6;
|
||||
/** The largest count the layout table covers (3x3). Never a cap by itself. */
|
||||
const TILE_LAYOUT_MAX = 9;
|
||||
// The smallest tile worth showing: about 60 columns and a dozen rows at the
|
||||
// default tile font. Bounds how many tiles a window can hold.
|
||||
const TILE_MIN_W = 480;
|
||||
@@ -1648,7 +1656,7 @@ const TILE_MIN_H = 240;
|
||||
// Three tiles go side by side (3x1) only when each still gets ~600px;
|
||||
// otherwise they take three cells of a 2x2.
|
||||
const TILE_GRID_WIDE_3X1 = 1800;
|
||||
// A tile's xterm keeps this many lines, not DEFAULT_SCROLLBACK: nine DOM
|
||||
// A tile's xterm keeps this many lines, not DEFAULT_SCROLLBACK: a grid of DOM
|
||||
// renderers at 50k lines each is a real memory cost, and a tile's load is a
|
||||
// bounded 1 MiB window anyway, so more scrollback only fills with live output.
|
||||
const TILE_SCROLLBACK = 10000;
|
||||
@@ -1664,7 +1672,7 @@ const TILE_FONT_SIZE_DEFAULT = 13;
|
||||
* @returns {{cols: number, rows: number, fits: boolean}}
|
||||
*/
|
||||
function computeTileLayout({ count, width = Infinity, height = Infinity, minTileW = TILE_MIN_W, minTileH = TILE_MIN_H }) {
|
||||
const n = Math.min(Math.max(0, Math.floor(Number(count) || 0)), TILE_GRID_MAX);
|
||||
const n = Math.min(Math.max(0, Math.floor(Number(count) || 0)), TILE_LAYOUT_MAX);
|
||||
let cols;
|
||||
let rows;
|
||||
if (n === 0) return { cols: 0, rows: 0, fits: true };
|
||||
@@ -2109,6 +2117,7 @@ if (typeof window !== 'undefined') {
|
||||
tileInDirection,
|
||||
cycleTile,
|
||||
TILE_GRID_MAX,
|
||||
TILE_LAYOUT_MAX,
|
||||
TILE_MIN_W,
|
||||
TILE_MIN_H,
|
||||
TILE_SCROLLBACK,
|
||||
|
||||
+34
-16
@@ -338,6 +338,25 @@ Object.assign(CodemanApp.prototype, {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* How many tiles the grid takes here and now: what the window fits, at most
|
||||
* the cap (TILE_GRID_MAX), never less than one. The texts say which of the
|
||||
* two binds, so a large monitor never reads "this window fits 6".
|
||||
*/
|
||||
_tileGridLimit() {
|
||||
const max = window.CodemanTileGrid.TILE_GRID_MAX;
|
||||
const fits = this._tileGridCapacityNow();
|
||||
const capacity = Math.max(1, Math.min(fits, max));
|
||||
const byCap = fits >= max;
|
||||
return {
|
||||
capacity,
|
||||
hint: byCap ? `Up to ${max} tiles` : `This window fits ${capacity} tile${capacity === 1 ? '' : 's'}`,
|
||||
full: byCap
|
||||
? `The grid holds at most ${max} tiles`
|
||||
: `The grid already holds what this window fits (${capacity})`,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* The Tiles button: with the grid open it closes it (back to the single view
|
||||
* of the focused session); otherwise it opens a picker with a checkbox per
|
||||
@@ -359,7 +378,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
if (!this.canOpenTileGrid()) return;
|
||||
const T = window.CodemanTileGrid;
|
||||
const capacity = Math.max(1, Math.min(this._tileGridCapacityNow(), T.TILE_GRID_MAX));
|
||||
const limit = this._tileGridLimit();
|
||||
const capacity = limit.capacity;
|
||||
const candidates = T.buildTilePickerSessions(this.sessions, this.sessionOrder, this.detachedSessions);
|
||||
const remembered = (this._readStoredTileGrid()?.ids || []).filter((id) => candidates.some((c) => c.id === id));
|
||||
const seed = remembered.length
|
||||
@@ -394,7 +414,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
footer.className = 'tile-picker-footer';
|
||||
const hint = document.createElement('span');
|
||||
hint.className = 'tile-picker-hint';
|
||||
hint.textContent = `This window fits ${capacity} tile${capacity === 1 ? '' : 's'}`;
|
||||
hint.textContent = limit.hint;
|
||||
const open = document.createElement('button');
|
||||
open.type = 'button';
|
||||
open.className = 'tile-picker-open';
|
||||
@@ -413,7 +433,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
const count = boxes.filter((b) => b.checked).length;
|
||||
for (const b of boxes) {
|
||||
b.disabled = !b.checked && count >= capacity;
|
||||
b.title = b.disabled ? `This window fits ${capacity} tiles` : '';
|
||||
b.title = b.disabled ? limit.full : '';
|
||||
}
|
||||
open.disabled = count === 0;
|
||||
};
|
||||
@@ -477,8 +497,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
this.closeTileAddMenu();
|
||||
const T = window.CodemanTileGrid;
|
||||
const capacity = Math.min(this._tileGridCapacityNow(), T.TILE_GRID_MAX);
|
||||
const full = grid.ids.length >= Math.max(capacity, 1);
|
||||
const limit = this._tileGridLimit();
|
||||
const full = grid.ids.length >= limit.capacity;
|
||||
const candidates = T.buildTilePickerSessions(this.sessions, this.sessionOrder, this.detachedSessions, grid.tiles);
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'tab-rail-action-menu tile-add-menu';
|
||||
@@ -497,7 +517,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
item.setAttribute('data-i18n-skip', '');
|
||||
item.textContent = c.label;
|
||||
item.disabled = full;
|
||||
if (full) item.title = `The grid already holds what this window fits (${capacity})`;
|
||||
if (full) item.title = limit.full;
|
||||
item.addEventListener('click', () => {
|
||||
this.closeTileAddMenu();
|
||||
if (this.addTile(c.id)) this.selectSession(c.id);
|
||||
@@ -515,7 +535,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
create.textContent = 'New session in this case';
|
||||
create.disabled = full || !theCase;
|
||||
if (!theCase) create.title = 'This session is not in a case';
|
||||
else if (full) create.title = `The grid already holds what this window fits (${capacity})`;
|
||||
else if (full) create.title = limit.full;
|
||||
create.addEventListener('click', () => {
|
||||
this.closeTileAddMenu();
|
||||
if (theCase) void this.runInCaseForTiles(theCase.name);
|
||||
@@ -570,10 +590,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
_joinTileGridFromRun(sessionId) {
|
||||
const grid = this._tileGrid;
|
||||
if (!grid?.open || grid.tiles.has(sessionId) || !this.sessions.has(sessionId)) return false;
|
||||
const T = window.CodemanTileGrid;
|
||||
const capacity = Math.max(1, Math.min(this._tileGridCapacityNow(), T.TILE_GRID_MAX));
|
||||
if (grid.ids.length >= capacity) {
|
||||
this.showToast?.(`The grid holds what this window fits (${capacity}): the new session opens on its own`, 'info');
|
||||
const limit = this._tileGridLimit();
|
||||
if (grid.ids.length >= limit.capacity) {
|
||||
this.showToast?.(`${limit.full}: the new session opens on its own`, 'info');
|
||||
return false;
|
||||
}
|
||||
// Run starts the session right after creating it: no Attach overlay
|
||||
@@ -899,13 +918,13 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (!this.canOpenTileGrid() || !this.sessions.has(sessionId) || this.detachedSessions?.has(sessionId)) {
|
||||
return false;
|
||||
}
|
||||
const T = window.CodemanTileGrid;
|
||||
const capacity = Math.max(1, Math.min(this._tileGridCapacityNow(), T.TILE_GRID_MAX));
|
||||
const limit = this._tileGridLimit();
|
||||
const capacity = limit.capacity;
|
||||
const grid = this._tileGrid;
|
||||
if (grid?.open) {
|
||||
if (!grid.tiles.has(sessionId)) {
|
||||
if (grid.ids.length >= capacity) {
|
||||
this.showToast?.(`The grid already holds what this window fits (${capacity})`, 'info');
|
||||
this.showToast?.(limit.full, 'info');
|
||||
return true;
|
||||
}
|
||||
this.addTile(sessionId);
|
||||
@@ -929,8 +948,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
openGroupAsTiles(groupId) {
|
||||
const group = (this.tabLayout?.groups || []).find((g) => g.id === groupId);
|
||||
if (!group || !this.canOpenTileGrid()) return false;
|
||||
const T = window.CodemanTileGrid;
|
||||
const capacity = Math.max(1, Math.min(this._tileGridCapacityNow(), T.TILE_GRID_MAX));
|
||||
const capacity = this._tileGridLimit().capacity;
|
||||
const ids = (group.refs || [])
|
||||
.filter((ref) => ref.kind === 'session')
|
||||
.map((ref) => ref.id)
|
||||
|
||||
Reference in New Issue
Block a user