Skip to content

fix(web): re-render only the live transcript row while streaming - #873

Closed
tt-a1i wants to merge 1 commit into
mainfrom
cursor/transcript-live-row-render-513e
Closed

tt-a1i wants to merge 1 commit into
mainfrom
cursor/transcript-live-row-render-513e

Conversation

@tt-a1i

@tt-a1i tt-a1i commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

Fixes #872

Problem

After #870 removed the history signature from streaming updates, a streaming token still re-rendered every history row in the Web transcript. Issue #872 records the headless Chrome measurement on 1000 messages of 1000 characters: React commit actualDuration stayed around 170ms median and input-to-frame around 220ms. That work repeats for unchanged history, so scroll, selection, and find all pay for rows that did not change. Related: #837, #870, #841.

Value

A live token should update the message that is actually growing. Unchanged history keeps its DOM, text selection, and findable copy. Tool output that arrives later still refreshes the call that owns it.

Approach

Each transcript entry caches the rows it produced, keyed by the inputs those rows close over (message identity, highlight, tool results and live tool state, turn, and the latest user prompt). A streaming update reuses the cached elements for unchanged entries and wraps them in a memoized frame, so those rows do not render again. The live entry misses the cache and renders. The row list, process groups, and turn structure stay the same DOM shape, so scroll following, selection, and in-page find keep their anchors.

List virtualization is not added. Memoized rows are enough to stop history renders, and a virtual window would risk the selection and find behavior the transcript already preserves.

Validation

  • bun run check
  • bun run test ui (100 files, 1534 tests)
  • tests/web/transcript-live-row-render.spec.ts checks visible history and live text, that the history DOM node stays the same node, that a changed history row renders, that an unchanged row before or after it does not, and that a new tool result appears on its call without rendering an unrelated history row. The render counter is the row component itself, so removing the memo fails the test. No timers, retries, or widened timeouts.

Headless Chrome 1280×800, Vite, the real Transcript, history messages of 1000 x characters, stable onResend. Each run records 20 streaming commits plus 12 input-to-frame samples (click to the second animation frame) and longtask entries.

This machine did not reproduce the 170ms / 220ms figures in #872. Markdown rows were already able to bail out at the leaf, so the unthrottled baseline here is a few milliseconds. The render count is the direct evidence that history rows stop rendering.

Rows Build Commit p50 Long tasks Input-to-frame p50 History row renders
250 before (c2aa6d3) 3.2ms 0 33.4ms re-renders with the parent
250 after 7.9ms 0 33.4ms 0 across 32 updates
1000 before (c2aa6d3) 9.1ms 4 (total 383ms) 33.5ms re-renders with the parent
1000 after 4.7ms 0 33.4ms 0 across 32 updates (32 live-row renders)

Input-to-frame sits on a ~33ms floor because the sample waits for two frames. At 1000 rows the steady commits fell from about 9ms to about 4.5ms and the long tasks on the first updates disappeared. At 250 rows the commit median did not fall; reconciling the memoized list costs more than re-running those already-cheap rows on this host. The 1000-row case is the one #872 measured.

Impact

  • User-visible behavior: streaming updates no longer rebuild unchanged history rows. Selection anchors and findable text stay put. A tool result still updates the call that displays it.
  • Model-visible context/tools: none.
  • Runtime/lifecycle: none. The cache is presentation state inside the transcript component and resets with the session scope.
  • Persisted config/data: none.
  • Compatibility or risk: rows after an entry recompute when their turn, prompt, or tool dependencies change. A callback identity change still rebuilds rows that close over that callback, matching the app's stable action object.
Open in Web Open in Cursor 

Streaming updates were rebuilding every history row. Cache each entry's
rendered rows and keep those elements memoized so only the live message
commits again.

Co-authored-by: tt-a1i <tt-a1i@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Web transcript still re-renders the whole history row tree on each streaming update

2 participants