Skip to content

feat(core): add scoped viewport preservation - #1357

Open
kitlangton wants to merge 7 commits into
anomalyco:mainfrom
kitlangton:feat/scoped-viewport-preservation
Open

feat(core): add scoped viewport preservation#1357
kitlangton wants to merge 7 commits into
anomalyco:mainfrom
kitlangton:feat/scoped-viewport-preservation

Conversation

@kitlangton

Copy link
Copy Markdown
Collaborator

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,3 to 0,1, then back to 2,3 33 ms later.

After: The application scopes preservation around the async prepend. ScrollBox restores the anchor after Yoga layout and before culling/drawing. Messages 2,3 remain visible through pagination; only later explicit page-up inputs move the viewport to 1,2 and 0,1.

How

  • packages/core/src/renderables/ScrollBox.ts stores one explicit child, its layout position, and the current scroll revision.
  • Content layout restores the positional delta before culling and drawing.
  • Any external scroll change cancels preservation, including scrolling away and back before layout.
  • Declarative callers can provide a stable direct-child id when reconciliation may replace the renderable instance.
  • Explicit preservation takes precedence over sticky scrolling while the scope is active.
  • packages/web/src/content/docs/components/scrollbox.mdx documents lifecycle, cancellation, and reconciled-child usage.
const preservation = scrollbox.preserveViewport(anchor, { id: anchor.id })
try {
  await prependContent()
  await layoutSettles()
} finally {
  preservation?.cancel()
}

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 passed
  • Node.js v26.4.0 bun run test:js:node: 4,618 passed, 6 skipped, 0 failed
  • bunx tsc -p packages/core/tsconfig.node-test.json --noEmit
  • bunx oxfmt --check packages/core/src/renderables/ScrollBox.ts packages/core/src/tests/scrollbox.test.ts packages/web/src/content/docs/components/scrollbox.mdx
  • bunx oxlint packages/core/src/renderables/ScrollBox.ts packages/core/src/tests/scrollbox.test.ts
  • git diff --check
  • OpenCode Drive: 12-turn session, fresh reader TUI, initial 20-message window, top pagination, and frame-by-frame terminal recording analysis

Demo

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
Loading

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.

2 participants