Skip to content

feat(tui): keep Next transcript rows between frames and add jump to start (#439) - #440

Open
BunsDev wants to merge 4 commits into
test/417-safety-matrixfrom
feat/439-next-virtualization
Open

BunsDev wants to merge 4 commits into
test/417-safety-matrixfrom
feat/439-next-virtualization

Conversation

@BunsDev

@BunsDev BunsDev commented Nov 3, 2026

Copy link
Copy Markdown
Member

Context

Measurements

Debug build, one developer machine, 80x24 frame. These are observations printed by next_workload.rs, not gates.

Content Before After
Committed answer, 1,000 lines ~1.7 ms ~1.0 ms
Committed answer, 100,000 lines ~90 ms ~1.1 ms
Streaming answer, 1,000 lines ~2.0 ms ~0.9 ms
Streaming answer, 100,000 lines ~94 ms ~1.2 ms

The first frame after a width or appearance change still builds every row once.

What still grows

The review measured these on the first commit, before the streaming change; the committed-message path they measure did not change.

  • Message comparison: each frame compares every message with the source of its rows. About 4.3 ms at 50,000 messages and 3.5 ms for 50 MB in one message.
  • Tool rows: built every frame, and Done and Error rows stay until /clear.
  • Memory: the kept sources hold a second copy of the conversation's text, on top of the wrapped rows. The first frame's work is no longer thrown away, so this memory now stays allocated.

Design

  • TranscriptRows keeps wrapped rows per message. A message is rebuilt when its displayed content, the width or the appearance changes.
  • Staleness is decided by comparing content, not through App::transcript_version. App::messages is public and written from many places, some of which do not bump the counter, and a missed signal would show wrong text.
  • A streaming answer keeps rows for its complete lines. Only the unfinished line is rebuilt, and rows are dropped if the text stops extending what they were built from, as in a retry. terminal_safe works one character at a time, so chunking gives the same rows; a test checks this at several chunk sizes with hostile bytes.
  • jumpToStart holds the reader at the first row through resizes and new output until it scrolls down. "New activity below" still shows.

Verification

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace: 2944 passed, 0 failed
  • tmux suite with a disposable HOME: 94/94, including Alt+G in the real terminal case
  • New tests:
    • a frame after any change matches a frame drawn from scratch (replaced text of the same length, truncate, clear, rewrap, density);
    • only a changed message is rebuilt;
    • streamed rows match whole-text rows;
    • the start holds through layout changes and is released by scrolling;
    • Classic leaves Alt+G as text input.

Independent review

A fresh reviewer read 14bad5b and probed it with an out-of-tree test crate. It found no stale-row, ordering, terminal_safe, panic or Classic defects. Resolved in the second commit:

Finding Resolution
Medium: frame cost was still linear while streaming (94 ms at 100,000 lines) Complete lines keep their rows
Medium: Alt+G landed at the bottom when nothing was measured (inspector hiding the transcript, after Alt+L) Explicit hold, not a measured offset
Low to medium: a resize moved the reader off the first row Same hold
Low: the code comment and first commit message overstated what stopped being linear Comment corrected; remaining costs listed above
Low: the Classic guard test did not press Alt+G Added

Left as is: Alt+G on a short transcript shows "scrolled up", the same as PageUp does. A selection survives Alt+G, as with every keybinding action.

Risk and Rollback

  • Risk level: medium. A bug here would show the wrong text in Next, which is why staleness is decided by comparing content and tested against fresh frames. Classic is not affected.
  • Rollback plan: revert this PR. No data or format migrates. The new alt+g default binding goes with it.

…tart (#439)

A frame re-projected and re-wrapped the whole conversation, so its cost grew
with transcript length. Committed messages now keep their wrapped rows, and a
frame copies only the visible ones. An 80x24 frame with a 100,000-line answer
goes from about 90 ms to about 1.1 ms in a debug build.

A kept row is rebuilt when the message's displayed content, the width or the
appearance changes. That is decided by comparing content rather than through
a change counter, since App::messages is written from many places and a missed
signal would show stale text. Live output and tool rows are still built every
frame.

Alt+G (jumpToStart, Next only) goes to the start of the transcript. Classic
leaves the chord unbound.
…hanges (#439)

The review found the original cost remained while an answer streamed: an 80x24
frame with 100,000 live lines still took about 94 ms. The complete lines of the
streaming answer now keep their rows and only the unfinished line is rebuilt, so
that frame takes about 1.2 ms in a debug build.

Alt+G could land at the bottom when nothing had been measured, and a resize
moved the reader off the first row. The reader now stays at the start until it
scrolls down, through resizes and new output.

Each frame still compares every message with the source of its rows and still
builds tool rows, and the kept sources hold a second copy of the conversation's
text. The earlier commit message and code comment overstated this; the comment
is corrected here.
@vercel

vercel Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
docs Ready Ready Preview Nov 3, 2026 5:14am UTC

Request Review

This branch was successfully deployed

1 active deployment
Preview — 6f16a8a7 Deployed Nov 3, 2026 by vercel[bot]
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.

1 participant