feat(core): add scoped viewport preservation - #1357
Open
kitlangton wants to merge 7 commits into
Open
Conversation
kitlangton
requested review from
Hona,
kommander,
msmps and
simonklee
as code owners
August 11, 2026 17:16
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.
What
Add an explicit
ScrollBoxRenderable.preserveViewport()scope that keeps one direct child at the same viewport-relative position while content before it is inserted, resized, or reconciled.This lets applications preserve a reader's position across asynchronous history pagination without continuously scanning ScrollBox children or emitting an intermediate shifted frame.
Before / After
Before: An application prepends older rows, Yoga lays out and draws them at the current scroll offset, then application-level compensation runs after layout. In the reproduced OpenCode pagination flow, emitted frames changed from messages
2,3to0,1, then back to2,333 ms later.After: The application scopes preservation around the async prepend. ScrollBox restores the anchor after Yoga layout and before culling/drawing. Messages
2,3remain visible through pagination; only later explicit page-up inputs move the viewport to1,2and0,1.How
packages/core/src/renderables/ScrollBox.tsstores one explicit child, its layout position, and the current scroll revision.idwhen reconciliation may replace the renderable instance.packages/web/src/content/docs/components/scrollbox.mdxdocuments lifecycle, cancellation, and reconciled-child usage.Scope
This PR adds only app-directed, scoped preservation. It does not add a persistent automatic anchoring mode or heuristics for selecting an anchor. The broader automatic implementation remains preserved in closed PR #1354 for possible future work.
The OpenCode call-site update remains in anomalyco/opencode#39721 and can adopt this API after the corresponding OpenTUI release.
Testing
bun test packages/core/src/tests/scrollbox.test.ts: 54 passedbun run test:js:node: 4,618 passed, 6 skipped, 0 failedbunx tsc -p packages/core/tsconfig.node-test.json --noEmitbunx oxfmt --check packages/core/src/renderables/ScrollBox.ts packages/core/src/tests/scrollbox.test.ts packages/web/src/content/docs/components/scrollbox.mdxbunx oxlint packages/core/src/renderables/ScrollBox.ts packages/core/src/tests/scrollbox.test.tsgit diff --checkDemo
Before: post-layout application compensation exposes the prepended rows
recording-647637bf-d0b7-4ef0-8aef-554cea3b55ef.mp4
After: scoped preservation restores before frame emission
recording-dafb9ac5-8941-4188-883a-3fb2f463a98c.mp4
Flow
sequenceDiagram participant App participant ScrollBox participant Yoga participant Renderer App->>ScrollBox: preserveViewport(anchor, id) App->>App: asynchronously prepend content Renderer->>Yoga: calculate layout Yoga-->>ScrollBox: updated child positions ScrollBox->>ScrollBox: restore anchor delta ScrollBox-->>Renderer: cull and draw stable viewport Renderer-->>App: emit frame App->>ScrollBox: cancel preservation