Context
packages/client/src/App.tsx is 666 lines on feat/canvas-only-ux (was 585 at branch base — +81 net across the canvas-only-ux refactor). Project rule says "Keep App.tsx as a thin layout shell" (.claude/rules/solidjs.md), but App.tsx is currently doing both layout wiring and render-prop materialization for per-tile chrome.
What's bloated
Three inline closures account for ~280 of the 666 lines:
renderTileTitleActions(id) — ~110 lines. Per-tile chrome (agent indicator, theme pill, split toggle, search button, screenshot button). Reads store, rightPanel, subPanel, activeThemeName, etc.
renderCanvasTileBody(id, active) — desktop body wrapper (TerminalContent + SubPanel + search overlay).
renderMobileTileBody(id, visible) — mobile equivalent.
These exist as inline closures because CanvasTile / MobileTileView use a render-prop pattern (good for keeping the canvas/tile shells generic), and App.tsx ends up as the "wiring layer" that knows how to materialize per-tile chrome from singleton hooks.
Proposal
Extract each closure into its own component file:
packages/client/src/canvas/TileTitleActions.tsx — props { id: TerminalId }, reads useTerminalStore, useRightPanel, useSubPanel directly.
packages/client/src/canvas/CanvasTileBody.tsx — props { id: TerminalId; active: () => boolean }.
packages/client/src/MobileTileBody.tsx — props { id: TerminalId; visible: () => boolean }.
App.tsx then passes:
```tsx
renderTileTitleActions={(id) => }
renderTileBody={(id, active) => }
```
Expected outcome
- App.tsx shrinks by ~150–180 lines.
- Each new component owns its own singleton reads, satisfying the existing
no-preference-prop-drilling rule and the spirit of "components own their behavior."
- No behavior change.
Out of scope
- The
TILE_BUTTON_CLASS constant — fine to leave in App.tsx or move into the extracted component, whichever reads cleaner.
selectTerminalFromPill and other small helpers that genuinely are wiring.
Surfaced during /code-police on PR for canvas-only-ux branch.
Context
packages/client/src/App.tsxis 666 lines onfeat/canvas-only-ux(was 585 at branch base — +81 net across the canvas-only-ux refactor). Project rule says "Keep App.tsx as a thin layout shell" (.claude/rules/solidjs.md), but App.tsx is currently doing both layout wiring and render-prop materialization for per-tile chrome.What's bloated
Three inline closures account for ~280 of the 666 lines:
renderTileTitleActions(id)— ~110 lines. Per-tile chrome (agent indicator, theme pill, split toggle, search button, screenshot button). Readsstore,rightPanel,subPanel,activeThemeName, etc.renderCanvasTileBody(id, active)— desktop body wrapper (TerminalContent+SubPanel+ search overlay).renderMobileTileBody(id, visible)— mobile equivalent.These exist as inline closures because
CanvasTile/MobileTileViewuse a render-prop pattern (good for keeping the canvas/tile shells generic), and App.tsx ends up as the "wiring layer" that knows how to materialize per-tile chrome from singleton hooks.Proposal
Extract each closure into its own component file:
packages/client/src/canvas/TileTitleActions.tsx— props{ id: TerminalId }, readsuseTerminalStore,useRightPanel,useSubPaneldirectly.packages/client/src/canvas/CanvasTileBody.tsx— props{ id: TerminalId; active: () => boolean }.packages/client/src/MobileTileBody.tsx— props{ id: TerminalId; visible: () => boolean }.App.tsx then passes:
```tsx
renderTileTitleActions={(id) => }
renderTileBody={(id, active) => }
```
Expected outcome
no-preference-prop-drillingrule and the spirit of "components own their behavior."Out of scope
TILE_BUTTON_CLASSconstant — fine to leave in App.tsx or move into the extracted component, whichever reads cleaner.selectTerminalFromPilland other small helpers that genuinely are wiring.Surfaced during /code-police on PR for canvas-only-ux branch.