ee1c493559e38750d38b6e0913a489b14ed91901
The transcript is a plain Column scrolled in reverse instead of a LazyColumn. Nothing about Compose was re-measuring text that had already been drawn -- a node that is still alive and whose constraints have not changed skips measurement outright, in MeasurePassDelegate.remeasure. What was throwing that away was disposal: a lazy list drops a row the moment it leaves the viewport, and the markdown tree, the measured lines and the cached paragraph go with it. Keeping the rows is the fix, and it is what a browser-based client does that we were not. Two scroll corrections go with it, each of which had a comment explaining a way it had been seen to fire at the wrong moment. The content now hangs from its newest end, so a page of older history extends the far end and moves nothing on screen, and an arriving message extends the end the viewport is already pinned to. Following the newest message is no longer an effect that notices and corrects; it is where the content is. The same goes for the keyboard opening, which was the case that used to get missed. Paging asks its question in pixels of scroll -- how far can the reader keep going before they run out -- which is what it was always about. Rows were the wrong unit twice: a fixed count of them is a distance only by accident, and counting screenfuls of rows fixed the size of that mistake without fixing its kind. A saved position is resolved to the row that now holds that seq before the layout is asked to put it back. The events behind a row regroup between the save and the reopen, so the seq that was a row's first is often no longer any row's first, and handing the layout the saved seq named a row that did not exist -- the position was never applied and the session opened at the newest end. Verified on the emulator against a real 1,200-event transcript: the position survives leaving and reopening, jump-to-latest arrives and stays followed, and the newest message holds its place against the composer as the keyboard opens and the draft grows. Scrolling measures the same as the lazy version did (2.6% vs 2.0% janky, p50 16ms, p90 21ms, no slow UI-thread frames either way) -- the cap and the off-thread parse had already taken that cost out, so this change is about what the list can no longer do wrong rather than about frames. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Languages
Rust
54%
Kotlin
43.6%
Shell
2.4%