Repository navigation
perf(tui): wrap the transcript lazily so resize and resume stay fast - #358
Merged
Merged
Conversation
A width change invalidated and re-wrapped every transcript block before the next frame could paint, and the first paint of a resumed session and a replay into a painted window wrapped the whole history the same way, so each of those frames grew with the session's length. Every frame also cloned each visible line twice (into a frame-sized buffer, then into the paragraph that painted it), and each wrapped row kept its own copy of the text it showed. Blocks now keep the painted width of each source line, measured once at push, and size themselves from it at any width: an estimate that is exact unless a line wraps and never exceeds the wrap. A width change, a first paint, or a replay re-measures every block and wraps only the ones the viewport shows; the rest are wrapped as they scroll into view. While an estimate turns exact, the offset grows with any block at or below the edge it names, and scrolls, folds, and removals wrap the rows they cross first, so every frame paints what wrapping everything up front would. Rows are painted straight from the block caches into the frame buffer by a line painter that matches the paragraph cell for cell, and a wrapped row's text is a byte range of its block's selectable text instead of a copy.
…ed scroll A press, drag, or release handled after wheel or page input but before the next paint selected and copied text from rows above the one under the pointer. Each painted row was remembered by its absolute row index. A scroll wraps the blocks it crosses, and a block that turns from its estimate to its real height moves the index of every row below it, so the last frame's indices named other text until the next paint. Painted rows are now remembered by block and row within the block, which other blocks' heights cannot move, and resolved to the current index when a pointer event or the selection overlay reads them. The frame comment also names the one exception to painting without clones: the selected block's rows.
…ed prompt A prompt rejected by a user_prompt_submit hook is removed from the transcript after the notices its turn produced may already be on screen. Painted rows named their block by index, and the removal shifts every later block down by one, so a press or drag handled before the next paint selected and copied the notice below the one under the pointer. Blocks now carry an id assigned in push order and never reused. A painted row records the id beside the index it had when painted; when the block at that index no longer has the id, the row is found by searching the ids, which removal keeps in order, and a row of the removed block names nothing.
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
Resizing the terminal re-wrapped every transcript block before the next frame could paint. The first paint of a resumed session and a replay into a painted window wrapped the whole history the same way. On a long session each of those frames grew with the session's length, so a resize or a resume stalled visibly. Steady frames also did avoidable work: every visible line was cloned twice per frame (into a frame-sized buffer, then into the paragraph that painted it), and each wrapped row kept its own copy of the text it showed.
Summary
ui::text::paint_line) that matches a paragraph cell for cell. The selected block's rows are the one exception: they are still cloned so the selection background can sit under them.No docs, CLI help or model-visible prompt bytes change.
Test Plan
app::tests::resize_first_paint_and_replay_wrap_only_the_blocks_in_view: a resize, a first paint and a replay over a long history wrap only the blocks the viewport shows.app::tests::steady_frames_allocate_nothing_per_visible_row: a steady repaint's allocations do not grow with the number of visible rows.app::tests::a_selection_after_an_unpainted_scroll_takes_the_painted_text: a press and drag handled after a scroll and before the next paint select the text that was painted under the pointer.app::tests::a_selection_after_a_rejected_prompt_takes_the_painted_notice: a press and drag handled after a hook rejects a prompt and before the next paint select the notice painted under the pointer, not the one that slid into its place.ui::transcript::tests::painted_rows_keep_their_text_when_a_block_above_is_removed: rows painted before a block above them is removed still resolve to the text they painted, and the removed block's rows resolve to nothing.app::tests::lazy_wrapping_paints_exactly_what_eager_wrapping_paints: compares every frame of a lazily wrapped app with one that wraps everything up front. It runs a scripted walk plus seeded random walks over a long and a short history, covering scrolls, resizes, arrivals, streaming, find, block navigation, folds, Ctrl+O and removing the last user message.ui::text::tests::paint_line_paints_exactly_what_a_paragraph_paints: covers wide, zero-width and control graphemes, overflow, line and span styles, alignment and a gutter prefix.paint_rows_paints_what_the_cloned_lines_paint: also checks that every recorded row reference resolves back to the line it painted.estimates_never_exceed_the_wrap_and_are_exact_when_lines_fit.row_maps_slice_exactly_the_text_they_count.measure_resize_and_first_paint_at_scalefor timing resize and first paint on a large history.cargo test --workspace --locked: pass.cargo +1.97.0 clippy --workspace --all-targets --locked -- -D warnings: pass.No outstanding findings block merging.
Summary
The PR makes transcript wrapping and painting lazy to keep resize, resume, and steady frames responsive. Painted rows now retain stable block IDs, so removing a rejected prompt does not make a mouse selection target a different notice.
Reviews (2) · Last reviewed commit: "fix(tui): keep a mouse selection on the ..."