style(tiles): bigger tile header buttons, the app header's icon size

Owner feedback: the tile header's ⋯ ⤢ + × read as tiny next to the
session name. The buttons inherited the header's 12px font. They are now
26px click targets (min-width, so a wider glyph still fits) with a 16px
glyph, the same as the app header's own icon buttons (.btn-icon-header);
the thin ellipsis and cross get 19px so all four read at one visual
size. The header grows from 24 to 28px to hold them, the inline rename
input to 22px. Checked live at DSF 1 on a dark (daylight-blue) and a
light (paper-gray) skin, focused and unfocused tiles, and a zoomed tile.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-06 23:27:08 +02:00
parent b7fafb1c16
commit 39d87e363f
4 changed files with 53 additions and 10 deletions
+1 -1
View File
@@ -809,7 +809,7 @@ Further detail: with many sessions the horizontal strip stops being scannable, w
⚠️ **Opening.** The Tiles button's click and `Ctrl+Shift+G` are one function, `toggleTileGrid` (owner decision 8), so the two cannot drift. It opens the grid at once on `tileGridOpenSet` (constants.js, pure): (a) the stored grid if any of its sessions survive, opened EXACTLY (`openTileGrid(..., { mergeSplit: false })`: an open split closes and its sessions do not join), (b) else an open split's two sessions, (c) else the picker's list (tab order, no detached sessions) up to `_tileGridLimit().capacity`, the active session always included and focused. Ctrl/Cmd+click with the grid closed opens the same set plus the clicked session. The picker is on right-click (`oncontextmenu`, which calls `preventDefault` for the browser menu); with the grid open it is preselected with the current tiles and Open REPLACES them (close without reselect, null `activeSessionId`, reopen, as "Open group as tiles" does).
⚠️ **Chrome.** The header is a fixed 24px sibling of the body, refreshed in place on every tab render (`_renderSessionTabsImmediate` wrapper), never by rewriting the tile (that would take its xterm along) and never growing (#464: the body holds the xterm). Header buttons stop pointerdown, so acting on an unfocused tile does not focus it. × removes the tile only; killing stays behind the session menu's Close session. The `needs` pulse animates `box-shadow` only. Dividers are their own 6px grid tracks (`gap: 0`), tiles placed explicitly; a drag uses pointer capture, reflows the affected tiles locally per animation frame and sends ONE `fit()` (one PTY resize) per affected tile at pointer-up; closing the grid or removing a tile mid-drag tears it down. A tab dragged onto a tile replaces it (or swaps two tiles); tiles and empty slots handle `dragover`/`drop` in the CAPTURE phase and stop it, because the drag carries the session id as text and xterm's helper textarea would type it into the PTY.
⚠️ **Chrome.** The header is a fixed 28px sibling of the body (its buttons are 26px targets with 16 to 19px glyphs, the app header's own icon-button size), refreshed in place on every tab render (`_renderSessionTabsImmediate` wrapper), never by rewriting the tile (that would take its xterm along) and never growing (#464: the body holds the xterm). Header buttons stop pointerdown, so acting on an unfocused tile does not focus it. × removes the tile only; killing stays behind the session menu's Close session. The `needs` pulse animates `box-shadow` only. Dividers are their own 6px grid tracks (`gap: 0`), tiles placed explicitly; a drag uses pointer capture, reflows the affected tiles locally per animation frame and sends ONE `fit()` (one PTY resize) per affected tile at pointer-up; closing the grid or removing a tile mid-drag tears it down. A tab dragged onto a tile replaces it (or swaps two tiles); tiles and empty slots handle `dragover`/`drop` in the CAPTURE phase and stop it, because the drag carries the session id as text and xterm's helper textarea would type it into the PTY.
⚠️ **Attach.** A tile whose session has no PTY (`pid === null`) or whose socket closed because it exited (4009) shows an Attach overlay (absolute, the body keeps its size): `POST /interactive` (or `/shell`) with NO body, one in flight per session, a tripped PTY-exit breaker only through the same confirm as the single view, then the tile is remounted (a stopped socket cannot reconnect). The routes report a refusal in the ENVELOPE of a 200, so the response body is read, not `res.ok`. An agent that exited in a live pane (`paneExit`) cannot be started again in place (both routes refuse while the pane's tmux client runs): its tile shows the exit and points at Close session.
+3
View File
@@ -29,6 +29,9 @@ or settled a question the spec left open. The invariants as built are in
B too), and a refresh clears the screen at its turn in the queue, so a waiting tile keeps
its last frame.
- **4009 lands on the Attach overlay**, and 4003/4004/4010 remove the tile.
- **Tile header buttons are 26px targets with 16 to 19px glyphs** (owner feedback: the
first build's 12px glyphs read as tiny next to the name), the size of the app header's own
icon buttons; the header grew from 24 to 28px to hold them.
- **The Tiles button opens the grid at once** (owner decision 8): a click (and
`Ctrl+Shift+G`, the same `toggleTileGrid`) opens `tileGridOpenSet` (constants.js): the
grid this tab last had, else an open split's two sessions, else the open sessions in tab
+26 -9
View File
@@ -19441,8 +19441,9 @@ html[data-session-list="sidebar"][data-sidebar="collapsed"] .btn-sidebar-toggle
holds the xterm, so a header that grew (a wrapped name, the rename input)
would shrink the body with nothing refitting the xterm or its PTY. */
.tile-header {
flex: 0 0 24px;
height: 24px;
/* Just tall enough for the 26px buttons below (and its 1px border). */
flex: 0 0 28px;
height: 28px;
box-sizing: border-box;
display: flex;
align-items: center;
@@ -19466,20 +19467,36 @@ html[data-session-list="sidebar"][data-sidebar="collapsed"] .btn-sidebar-toggle
.tile-actions {
display: flex;
flex: 0 0 auto;
gap: 1px;
gap: 2px;
}
/* The click target and glyph size of the app header's own icon buttons
(.btn-icon-header: 26px square, 16px glyph). Inheriting the header's 12px
made the glyphs read as tiny next to the session name. */
.tile-btn {
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
min-width: 26px;
height: 26px;
padding: 0 4px;
border: none;
background: none;
color: inherit;
font: inherit;
line-height: 18px;
height: 20px;
padding: 0 6px;
font-family: inherit;
font-size: 16px;
line-height: 1;
border-radius: 3px;
cursor: pointer;
opacity: 0.65;
opacity: 0.7;
}
/* The ellipsis and the cross are thin glyphs: a step up so all four read at
the same visual size as the arrow and the plus. */
.tile-btn.tile-menu,
.tile-btn.tile-remove {
font-size: 19px;
}
.tile-btn:hover,
@@ -19492,7 +19509,7 @@ html[data-session-list="sidebar"][data-sidebar="collapsed"] .btn-sidebar-toggle
.tile-rename-input {
flex: 1 1 auto;
min-width: 0;
height: 18px;
height: 22px;
box-sizing: border-box;
padding: 0 4px;
font: inherit;
+23
View File
@@ -138,6 +138,29 @@ describe('header buttons', () => {
});
});
describe('header button size (owner feedback: the 12px glyphs read as tiny)', () => {
const css = readFileSync(resolve(import.meta.dirname, '../src/web/public/styles.css'), 'utf8');
const rule = (selector: string) => {
const at = css.indexOf(`${selector} {`);
return at === -1 ? '' : css.slice(at, css.indexOf('}', at));
};
it("buttons are the app header's icon size: 26px targets, a 16px glyph, never the header's inherited 12px", () => {
const btn = rule('.tile-btn');
expect(btn).toContain('min-width: 26px;');
expect(btn).toContain('height: 26px;');
expect(btn).toContain('font-size: 16px;');
expect(btn).not.toMatch(/\bfont: inherit;/);
});
it('the thin glyphs (ellipsis, cross) get a step more, and the header holds the buttons', () => {
expect(rule('.tile-btn.tile-menu,\n.tile-btn.tile-remove')).toContain('font-size: 19px;');
const header = rule('.tile-header');
expect(header).toContain('flex: 0 0 28px;');
expect(header).toContain('height: 28px;');
});
});
describe('rename', () => {
function startRename(app: GridApp, id: string) {
headerOf(id).children[1].dispatch('dblclick', { stopPropagation: vi.fn() });