feat(tiles): the grid controller, and the main terminal parked while it is open

tile-grid.js (load order 7.6) adds the grid to CodemanApp: openTileGrid,
closeTileGrid, addTile, removeTile and _selectTiledSession, over a
<section class="tile-grid"> that is a SIBLING of .terminal-wrap and takes its
place under .main.tiles-active. Every tile is a TerminalTile with the grid's
one load queue, TILE_SCROLLBACK, a bounded load and its own per-device font
size (codeman-tile-font-size; Ctrl +/- sizes the tiles while the grid is open).
Layout comes from computeTileLayout; one ResizeObserver on the section refits
each tile (xterm and PTY together) on the trailing edge.

Opening parks the main terminal: _cleanupPreviousSession runs once (its
snapshot is right at that moment, and it closes the main socket), and
activeSessionId always names the focused tile's session, so the panels follow
focus. With the main socket closed, every main-terminal path that would write
the focused tile's output into the hidden xterm, fetch a capture for it,
resize it or reopen its socket now stands aside through _tilesOwnTerminal():
the SSE terminal, clear and refresh handlers, the dropped-output recovery,
the completion/error writelns, retryConnection and handleInit (both re-arm the
tiles instead; handleInit keeps live tiles and drops dead ones), sendResize,
throttledResize, the history re-pull, and the WebGL long-task observer, which
watches the whole page and must not count tile renders toward the main
terminal's sticky WebGL disable. The header connection state comes from the
tile sockets.

Closing destroys every tile, invalidates the main terminal's cached content
(snapshot, codeman-xs key, buffer cache) for every tiled id, since it predates
the grid, and replays the focused session fresh in the single view.
_focusedPane() answers with the focused tile and _forEachTile reaches every
grid tile. A tile whose socket stops for good is removed (4003, 4004, 4010) or
keeps its "session ended" marker (4009).

No entry point yet: the grid is opened from the shortcut registry in a later
commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-06 16:20:05 +02:00
parent fca7acd05f
commit 4341d3dca8
7 changed files with 1207 additions and 16 deletions
+72
View File
@@ -19347,6 +19347,78 @@ html[data-session-list="sidebar"][data-sidebar="collapsed"] .btn-sidebar-toggle
min-height: 0;
}
/* ── Tile grid (tile-grid.js, docs/tile-grid-plan.md) ──────────────────────
1 to 9 sessions side by side, each a TerminalTile. The section is a SIBLING of
.terminal-wrap; .main.tiles-active swaps one for the other (the main terminal
is parked while the grid is open). Columns and rows are set inline by
_applyTileLayout() from the tile count. */
.tile-grid {
display: none;
}
.main.tiles-active .terminal-wrap {
display: none;
}
.main.tiles-active .tile-grid {
display: grid;
flex: 1;
min-width: 0;
min-height: 0;
gap: 4px;
padding: 4px;
background: var(--bg-dark);
}
/* A web tab takes the stage from the grid exactly as from the single view: the
tiles stay connected, and a hidden tile measures nothing and sends no
resize. AFTER the rule above, which has the same specificity. */
.main.webview-active .tile-grid {
display: none;
}
.tile {
position: relative;
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
overflow: hidden;
border: 1px solid var(--border-color, #333);
border-radius: 4px;
background: var(--term-bg, #161b23);
}
/* The focused tile has the keyboard and is the active session. */
.tile.focused {
border-color: var(--accent-color, #4a9eff);
}
.tile-body {
flex: 1 1 auto;
position: relative;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.tile-body .xterm {
width: 100%;
height: 100%;
}
/* Quiet "loading" state while the tile's capture waits in the grid's one load
queue (or runs). */
.tile.tile--loading .tile-body::after {
content: 'Loading\2026';
position: absolute;
top: 6px;
right: 10px;
font-size: 11px;
opacity: 0.6;
pointer-events: none;
}
/* Split-picker menu: a small popover listing sessions to split with, appended
to document.body and positioned `fixed` by openSplitPicker() (JS sets
top/right against the Split header button's own rect). z-index above the