perf(chat): virtualize the timeline by segment, not by turn - #547
Merged
Merged
Conversation
A turn is not a list row any more; each of its segments (a message, an error, a divider, a Work Log run) is, and the turn's trailer renders on its last row. A running turn with hundreds of tool runs and interim notes costs the rows on screen every frame instead of the whole turn. Row sync aligns old and new rows by the identity of their first entry and splices rows in place, so a run keeps its measured height while it streams and a steer accepted mid-turn inserts a row rather than resetting the list. Markdown residency counts rows instead of turns and no longer pins every document of the running turn. Timeline::fold_stored folds the client's held records by reference; a history page used to clone the whole held log before refolding it.
yermakoffivan
pushed a commit
to yermakoffivan/tcode
that referenced
this pull request
Sep 29, 2026
Since the timeline is virtualized by segment (Tryanks#547), a row read its turn's entries from its first row's start to its last row's end. A pending steer belongs to no segment, so one that followed a message fell outside that span and the trailer never drew it: the steer vanished until the next tool run, whose range reaches the turn's end, covered it again. The turn's span now comes from the timeline itself.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Thread
9553a8bb…(7.6 MB, 19k events, 13 turns) is unusable on a phone. The transport work (compressed wire, 4 KiB output previews, 512 KiB pages) and the list virtualization both key on the turn, and this thread has one still-running turn holding 985 items (345 commands, 224 assistant notes, 179 reasoning items) that folds into 449 segments. One list row = one turn, so every frame rebuilt those 449 segments, and Markdown residency counted its 8-turn margins over a 13-turn thread, keeping essentially every document parsed.Reference: Zed's agent thread view (
crates/agent_ui/src/conversation_view/thread_view.rs) gives GPUI'sListStateone row per thread entry, never per turn, and splices rows in place as entries land.What changes
TimelineRow(wasTurnListItem) is a segment of a turn: a message, an error, a divider or a Work Log run. The turn's trailer (plan card, changed files, liveness/finished time, pending steers) renders on the turn's last row; a turn without segments keeps one empty row.segment_entriesnow also reports each segment's entry range so indexing and rendering fold the same way.ListState::splicekeeps the anchor), rows above everything are the existingPrependpath with its history reservation, and rows that vanish or reorder still reset, except inside the partial first turn. A steer accepted mid-turn is now an in-place insertion rather than a reset.Timeline::fold_storedfolds the client's held records by reference; each history page used to clone the whole held log (≈2 MB here) before refolding.Turn-keyed behaviour is unchanged: expansion keys,
open_diff_for_turn, rewind, palette jump and the highlighted turn all resolve throughrows_of_turn.Tests
a_long_running_turn_virtualizes_into_one_row_per_segment(model): a 200-step running turn indexes to 402 rows with the live/last-assistant flags on the right rows, and the next command grows the last row in place.a_long_running_turn_parses_only_the_markdown_near_the_viewport(view): a single 150-note running turn opens with fewer than 60 documents resident andnote-0not parsed. Fails before this change (151 resident).Checks run locally:
cargo fmt --all --check,cargo clippy --workspace --all-targets --locked -- -D warnings,cargo nextest run --workspace --locked. Not exercised: iOS/Android/Web builds (CI), and a real phone against the affected thread.