feat(grid): desktop split-grid watch board — v1 (single/1×2/2×2)
When several sessions are open on a large screen (≥1024px), the terminal
area can split into 1×2 or 2×2 so multiple LIVE, interactive terminals show
at once — a monitoring convenience for vibe-coding several Claude sessions.
Design keeps activeIndex as the single focused pane, so keybar/voice/approval
routing is unchanged; a new gridLayout + derived visible-set lets several
.term-cell wrappers show together inside a CSS-grid #term. The server and WS
protocol are untouched.
- public/grid-layout.ts (new): layout types/capacity, visibleIndices,
matchMedia desktop gate (GRID_MIN_WIDTH=1024), persistence, toolbar toggle.
- tabs.ts: pane→.term-cell wrapper (header + terminal + inline-approve footer);
applyLayout() owns show/hide + grid class + cell order + placeholders;
board-aware activate() (never focuses a hidden pane); setFocused/setGridLayout
delegate to it; renderCell/renderInlineApprove; refitVisible; notification
suppression for on-screen panes (factoring in the home overlay).
- terminal-session.ts: show({focus}) so non-focused quadrants don't steal
keyboard focus; onFocus callback (capture-phase pointerdown).
- main.ts: mount the toggle; window-focus refit → refitVisible.
- style.css: cell/grid model (.term-pane → relative flex child), focus ring,
pending pulse, inline approve, placeholder, toggle + coarse-pointer targets.
- tests: grid-layout.test.ts + split-grid block in tabs.test.ts (+33).
Adversarial review (4 lenses → per-finding verify) caught and fixed a HIGH:
activate() was not board-aware, so opening a tab on a full grid focused a
hidden pane (typing into an invisible session). Verified: typecheck + build:web
clean, 1566 tests pass, grid-layout 95% / tabs 94% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import { mountDashboard } from './dashboard.js'
|
||||
import { mountHistory } from './history.js'
|
||||
import { mountShortcuts } from './shortcuts.js'
|
||||
import { mountShareSession } from './share.js'
|
||||
import { mountGridToggle } from './grid-layout.js'
|
||||
|
||||
const paneHost = document.getElementById('term')
|
||||
const tabs = document.getElementById('tabs')
|
||||
@@ -50,12 +51,12 @@ mountKeybar((data) => app.sendToActive(data), {
|
||||
onVoiceTrigger: (action) => app.handleVoiceTrigger(action),
|
||||
})
|
||||
|
||||
// Multi-device: when this device regains focus, re-assert the active tab's size
|
||||
// so a shared session snaps to THIS screen (latest-writer-wins) — full-screen on
|
||||
// whichever device you're currently using.
|
||||
window.addEventListener('focus', () => app.refitActive())
|
||||
// Multi-device: when this device regains focus, re-assert the size of every
|
||||
// VISIBLE pane so a shared session snaps to THIS screen (latest-writer-wins) —
|
||||
// full-screen on whichever device you're using (all quadrants in a split grid).
|
||||
window.addEventListener('focus', () => app.refitVisible())
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden) app.refitActive()
|
||||
if (!document.hidden) app.refitVisible()
|
||||
})
|
||||
|
||||
// Toolbar utilities.
|
||||
@@ -81,6 +82,15 @@ mountHistory(toolbar, {
|
||||
mountShortcuts(toolbar)
|
||||
mountShareSession(toolbar, () => app.activeSessionId())
|
||||
|
||||
// Split-grid layout toggle (desktop-only; hidden below GRID_MIN_WIDTH). The
|
||||
// control drives app.setGridLayout and is registered back so a programmatic
|
||||
// layout change (e.g. auto-fallback on window narrow) re-syncs its pressed state.
|
||||
const gridToggle = mountGridToggle(toolbar, {
|
||||
getLayout: () => app.getGridLayout(),
|
||||
setLayout: (layout) => app.setGridLayout(layout),
|
||||
})
|
||||
app.setGridToggle(gridToggle)
|
||||
|
||||
// Session management lives on the home Sessions chooser now (open / kill /
|
||||
// new), so the standalone /manage.html page was removed.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user