facebook / facebook/astryx

[RFC] ChatMessageList / ChatLayout scaling story for long agent transcripts

Open
#4,102 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
13k
Forks
1.1k
Avg merge
1d 15h
Merged PRs (30d)
690

Description

### Problem Statement

Agent transcripts grow without bound. Context compaction removes the natural cap on session length (a session continues across many context windows), and increasingly autonomous agents run long tool-heavy tasks. `ChatMessageList` renders the full history in plain DOM flow — by design, and it's the right default: native selection, find-in-page, and scroll anchoring are why we build on it. But there is no documented scaling envelope and no sanctioned mitigation path when transcripts get long. Builders of AI transcript UIs hit two walls with no first-party answer: session-switch mount latency, and live-resize reflow cost.

### Evidence of Demand

Measured on a production astryx-based AI desktop client, with a reproducible single-file harness (same astryx Markdown components in both arms; corpus calibrated from 182 real agent sessions — 70% folded tool groups, 5-7% code fences, 6-13% tables):

- **Memory and mount scale linearly, ~27 KB per rendered item**: 1k items ≈ 54 MB / 0.95 s mount; 5k ≈ 162 MB / 2.7 s; 20k ≈ 566 MB / 12.8 s mount, 15.8 s to stable bottom.
- **Live-resize reflow: 16 fps at 2k items** (a virtualized reference runs 60 fps on the same corpus).
- Anchoring is ChatLayout's **win**: zero drift during width reflow while scrolled up (native anchoring), where compensation-based virtualizers needed years of fixes.
- Today's workloads mostly fit: across 182 real sessions, tool-call folding absorbs 77% of lines and every session lands ≤3.4k rendered items. But the largest sessions were **compacted 12-22×** — transcript length is already decoupled from context limits, and agent autonomy keeps shifting the distribution right.
- Industry signal: claude.ai's transcript is virtualized today (TanStack Virtual; migrating to an in-house document-flow virtualizer as of Aug 2026 — see comments); Codex ships a custom bottom-anchored virtualizer; react-virtuoso monetized its AI MessageList (license: Commercial). ~~No design system ships a chat-capable virtualizer — Spectrum/antd/Fluent/Prime virtualize tables and option lists only.~~ _Update Aug 2026: Adobe now ships a virtualized chat Thread in `@react-spectrum/ai` (see comments); antd (Ant Design X), IBM Carbon (`@carbon/ai-chat`), and AWS Cloudscape ship chat components without transcript virtualization; Fluent/Prime still virtualize tables and option lists only._

### Why Existing Components Don't Cover This

- `ChatMessageList` renders all children; there is no windowed-data or load-earlier affordance.
- Fixing it from outside breaks: per-row `content-visibility: auto` with naive estimates measured **3× worse** mount at 5k and a renderer freeze at 20k — c-v needs coordinated intrinsic-size estimates and near-viewport exemptions that a consumer can't supply cleanly from outside the list.
- Rolling your own virtualization around ChatLayout means two writers to the same scroll position (app-side follow vs. the list's size compensation). This is workable — production AI clients do it — but invites subtle, hard-to-reproduce artifacts; a first-party answer can coordinate the two.

### Rough Approaches Considered

**Option A — data-layer affordance**: first-party "load earlier" / windowed-data support on `ChatMessageList`. Keeps plain DOM flow and all native semantics; matches the production pattern Claude Desktop uses for large sessions (lazy history + spinner).

**Option B — opt-in plugin, per the Table precedent (#3639 / #3942)**: `useChatVirtualization`, core stays non-virtualized. Source-level study of TanStack Virtual, LegendList (web), and virtua surfaced three design considerations: coordinating follow with size compensation inside the list closes off a class of intermittent artifacts (LegendList's state machine; TanStack's `followOnAppend`); a component-owned container eliminates the clamp-race class (virtua); and `anchorTo`/`followOnAppend` is production-proven API vocabulary (TanStack, used by claude.ai). Dependency posture can follow the plugin model: the plugin package carries any external dep; core stays zero-dep.

**Non-goal**: virtualizing core `ChatMessageList` by default — it would trade away find-in-page, selection, and native anchoring, which are the reasons to choose astryx Chat.

### Accessibility Considerations

The non-virtualized default *is* the accessibility feature: the full transcript stays in the accessibility tree, and find-in-page, select-across-messages, and native scroll anchoring all work. `ChatMessageList` maps to the ARIA `log` pattern (`role="log"`, polite live announcements on append).

Each option must preserve this bar:

- **Option A**: focus stays on the trigger after prepend; the viewport must not jump (visible-content position maintained).
- **Option B**: off-screen rows leave the AT tree, so the plugin must supply position context (`aria-setsize`/`aria-posinset` or `aria-rowcount`), retain focus when a focused row unmounts, and keep Home/End/PageUp/PageDown working across unrendered ranges. React Spectrum's Virtualizer is prior art for a11y-correct virtualization. Find-in-page and select-all remain fundamentally degraded under virtualization — which is why it stays opt-in.

### Performance Considerations

Measured with a reproducible harness (real-mix corpus as above; dual sampling — pre-paint rAF + post-paint — so only user-visible artifacts are counted; memory via `performance.measureUserAgentSpecificMemory` under cross-origin isolation):

- Today: N items ≈ **48N DOM nodes, ~27 KB/item**, linear. 1k ≈ 54 MB / 0.95 s mount; 5k ≈ 162 MB / 2.7 s; 20k ≈ 566 MB / 12.8 s. Live-resize: 16 fps at 2k items.
- **Option A**: nodes bounded by window size W (≈48W); full native semantics retained.
- **Option B**: O(viewport) — a few hundred nodes at any N; the virtualized reference held 60 fps resize and zero anchor drift on the same corpus.
- Anti-pattern, measured: naive per-row `content-visibility: auto` (coarse estimates) was 3× worse at 5k mount and froze the renderer at 20k — motivating a coordinated first-party implementation rather than consumer-side CSS.

### Pre-submission Checklist

- [x] I have read the Contributing guide
- [x] I have read the API Conventions
- [x] I have checked that existing Astryx components cannot compose to solve this
- [x] This is a general-purpose UI pattern (not specific to one product)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.