Final step of the virtualization plan. Turns the virtualized render
path on in production behind a row-count threshold, and adds regression
tests covering every gate.
- `VIRTUALIZATION_ROW_THRESHOLD = 60`. Short lists stay on the direct
render path (no wrapper, no position: absolute, no measurement
churn). Above the threshold the virtualizer takes over. Threshold is
sized so conversations under ~one session of activity don't pay the
virtualization cost; it activates once scrolling through a longer
history.
- `shouldVirtualize` now requires `renderRows.length >= threshold` in
addition to the existing opt-in and scroll-ref checks.
- `MessagesPanel` opts into virtualization for every layout it wires
(inline / sidebar / bottom-sheet). The internal threshold then
decides when to actually enable it, so callers don't need per-layout
heuristics.
- Tests: adds a new `ActivityTimeline virtualization threshold` block
covering (a) below-threshold list stays on the direct path,
(b) no viewport → direct path regardless of count, (c) above
threshold + viewport with `virtualizationEnabled` flips to the
virtualized render path (simulated by clicking "show all" past
pagination).
With this in, #70 → #74 combine to deliver:
- correct IntersectionObserver roots in scroll containers
- atomic render rows with stable keys
- windowed rendering with DOM-measured scrollMargin and measureElement
- auto-on when the cost of direct rendering actually shows up