GCWing / GCWing/OpenBitFun

[Bug]: Transcript re-measures every frame while a thinking card grows, causing stutter during reasoning

Open
#3,069 0 comments 0 reactions 1 assignee Claimed by @wsp1911 View on GitHub
Dominant language
Rust
Stars
2.2k
Forks
229
Avg merge
2h 46m
Merged PRs (30d)
577

Description

### Summary

While the model is reasoning, the thinking card grows from zero up to its `max-height: 300px` cap. **Every streamed line changes the card’s outer height**, and that height is transcript layout — so the virtualized message list re-measures and re-commits on each of those frames.

Expected instead: the card’s own growth should not cost the message list a re-measure every frame.

Measured on an ordinary reasoning round (**no user input at all**), `tailFollow.thinking` reported **13–52 list commits per second**, with the tail follow falling **up to 1034px** behind the content and then catching that up in a single visible move. To the reader this reads as the transcript sticking, then juddering.

This happens with **no scrolling by the reader**. It is independent of the `follow-output-owns-the-viewport` refusals logged during streaming — that line is the register recording our own placement, not a refused gesture.

### Area

Web UI

### Reproduction or evidence

**Steps**

1. Open a session and send a prompt that makes the model reason at length (a long chain-of-thought, not merely a long answer).
2. Do **not** touch the scrollbar or wheel — just watch the transcript while the thinking card streams.
3. Observe: the transcript hitches, and the follow visibly lags behind the streaming text and then jumps to catch up.

**Evidence — the repo already emits the numbers**

`tailFollow.thinking` / `tailFollow.list` come from `noteTailFollowStep` (`src/web-ui/src/infrastructure/diagnostics/flowChatTailFollowDiagnostics.ts`), sampled by `ModelThinkingDisplay.tsx` (subject `thinking`) and `useFlowChatFollowOutput.ts` (subject `list`). One reasoning round, before the fix below:

tailFollow.thinking windows=55
steps = 1995
snaps = 1248 # steps that changed the card outer height
innerScrollSteps = 751 # steps that scrolled inside a capped card (38%)
listCommits = 1425 # list re-measure + React commit
max listCommitsPerSec = 50.8
maxLagPx = 1034
travelPx = 5216

**Root cause**

`ModelThinkingDisplay.scss` sets `max-height: 300px; overflow-y: auto;`, so the card only starts scrolling internally **after** it has grown to 300px. Before that, every streamed line changes the card outer height. The component own comment acknowledges the cost:

> Below the card `max-height` the box is still growing, so each of these steps also costs the list a re-measure. Above it, none do.

`innerScrollSteps` being only 38% of `steps` is that gap.

**Proposed fix**

Pin the box to the cap for the whole stream, so the height stops changing and the remaining scrolling happens inside the card:

```scss
--thinking-content-cap: 300px;
max-height: var(--thinking-content-cap);

.thinking-content[data-streaming="true"] {
height: var(--thinking-content-cap);
}
```

Plus one line in `ModelThinkingDisplay.tsx` because the typewriter reveal also drains content over its own frames: `data-streaming` on the content element changes from `isActive` to `isVisuallyStreaming` (`FlowTextBlock.tsx` and `ModelRoundItem.tsx` already use it).

**After the change** (same instrumentation, same machine):

| metric (tailFollow.thinking) | before | after |
| --- | --- | --- |
| snaps | 1248 | 126 (0 in settled windows) |
| innerScrollSteps share | 751/1995 (38%) | 939/1065 (88%) |
| listCommits | 1425 | 163 |
| max listCommitsPerSec | 50.8 | 10 |
| maxLagPx | 1034 | 47 |

**Tradeoff**: the card now occupies its full 300px cap from the first streamed line, so a round whose reasoning is shorter than 300px leaves blank space under the text until the card collapses.

**Related but distinct**: #2891 pauses the tail follow on a reader upward scroll — that is about the reader gesture; this is about the cost of the card own growth, which happens with no gesture at all.

### Environment, if relevant

OpenBitFun version/commit: 1.0.0, local dev build from commit bb64534
OS: macOS (arm64)
Browser/device: Tauri desktop (WebKit)
Model/provider: DeepSeek V4 Pro

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.