anomalyco / anomalyco/opencode

Root cause analysis: viewport drifts while reading a backlog during generation (ref #29094)

Open
#41,243 1 comment 0 reactions 1 assignee View on GitHub

@simonklee is already working on this.

Since Aug 8, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Description

This issue documents the root cause analysis (RCA) for the long-standing viewport drift reported in #4196 / #29094, plus the fix. When the user scrolls up to read earlier messages while the agent is producing output, the reading position drifts down the transcript on every new message.

Root cause

The OpenTUI scrollbox sticky-scroll logic is correct — verified with two reproduction tests (core-level and solid render-level): while scrolled up, _hasManualScroll is set and scrollTop is held stable across in-place streaming, message growth below the reading position, and message appends.

The actual cause is the 100-message pruning in packages/tui/src/context/sync.tsx. message.updated keeps at most 100 messages per session:

if (updated.length > 100) {
  const oldest = updated[0]
  batch(() => {
    setStore("message", ...produce((draft) => draft.shift()))
    setStore("part", ...delete draft[oldest.id])
  })
}

Once a session exceeds 100 messages, every new message shifts the oldest message off the top. That shrinks scrollbox content above the reading position. The scrollbox correctly holds scrollTop fixed while scrolled up, so the reading position drifts down by the height of each pruned message. This happens on every new message in a long session, matching the symptom.

A render-level repro confirmed the drift: with 100 messages, scrolled up 10 lines, a message pinned at viewport line 2 moved to line -13 after only 5 prunes.

Note: the earlier fix attempt (commit 867f79fa3, stickyScroll={!userScrolled()}) exists only on branch fix/tui-scroll-when-user-scrolled-up and was never merged. Disabling sticky scroll entirely is the wrong fix — it breaks follow-the-stream when the user scrolls back down.

Fix

Compensate the scroll position when the oldest messages are pruned:

  • packages/tui/src/util/scroll.ts: compensatePruneScrollTop returns a scrollTop moved up by the height of content that disappeared above the first surviving message, computed from the layout captured before the prune.
  • packages/tui/src/routes/session/index.tsx: runs it in a createComputed watching the message list, so it runs before the list re-renders while the pre-prune layout is still available. Sticky-bottom following is untouched.
  • packages/tui/test/util/scroll-prune.test.tsx: unit + render tests. The render test asserts the message at the top of the viewport stays at the same viewport line across prunes; without the fix it drifts from y = 2 to y = -16.
Steps to reproduce
  1. Run a session long enough to exceed 100 messages (or otherwise trigger the prune).
  2. Scroll up to read a backlog while the agent is producing output.
  3. Observe the reading position drift down on each new message.
Operating System

any (reported on macOS and Linux)

Terminal

Ghostty, WezTerm, xfce4-terminal (per #4196 commenters)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.