A scroll within the 100ms persist debounce could otherwise have its queued
update fire after the user switched teams, writing the previous team's offset
under the new team and restoring the sidebar to the wrong position. Capture the
team that scheduled the update and drop it on fire if the active team changed.
Checkpoint of in-progress work:
- renderer: team messages panel/composer, messagesPanelLogic, teamSlice,
AnimatedHeightReveal plus their tests
- main: runtime process usage-stats caching (ignoreCachedMisses, bounded
eviction), alive-run-id helpers, team watch-scope notify wiring
Note: the getTeamAgentRuntimeSnapshot rssBytes expectation in
TeamAgentLaunchMatrix.safe-e2e is environment-dependent and still red.
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
Second step of the virtualization plan. No virtualization yet. This PR
makes IntersectionObserver-based visibility tracking correct inside
scroll containers (sidebar, bottom-sheet), which is a prerequisite for
virtualizing the timeline.
- Introduces `TimelineViewport` — a grouped contract passed as a single
`viewport` prop on `ActivityTimeline`. Holds `scrollElementRef`,
`observerRoot`, `scrollMargin`, and `virtualizationEnabled`.
- `MessageRowWithObserver` and `LeadThoughtsGroupRow` now create their
`IntersectionObserver` with `root = observerRoot?.current ?? null`
instead of defaulting to the document viewport. Unread marking now
fires when rows enter their real scroll parent.
- `MessagesPanel` resolves the active scroll owner from `position`
(inline from parent ref, sidebar from `sidebarScrollRef`, bottom-sheet
from `bottomSheetScrollRef`) and passes it into ActivityTimeline.
- Tests: stubs `IntersectionObserver` to capture `options.root` and
asserts null when no viewport is passed, and the provided element when
`viewport.observerRoot` is set.
`scrollMargin` and `virtualizationEnabled` are included in the contract
but not consumed yet — they land in follow-up PRs (#4/#5).