diff --git a/docs/architecture-invariants.md b/docs/architecture-invariants.md index f86dd412..9d6669cf 100644 --- a/docs/architecture-invariants.md +++ b/docs/architecture-invariants.md @@ -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. diff --git a/docs/tile-grid-plan.md b/docs/tile-grid-plan.md index d05ec8ba..37559654 100644 --- a/docs/tile-grid-plan.md +++ b/docs/tile-grid-plan.md @@ -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 diff --git a/src/web/public/styles.css b/src/web/public/styles.css index a3da0dbb..4b365818 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -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; diff --git a/test/tile-grid-chrome.test.ts b/test/tile-grid-chrome.test.ts index 4afce6a9..e86e3822 100644 --- a/test/tile-grid-chrome.test.ts +++ b/test/tile-grid-chrome.test.ts @@ -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() });