Skip to content

tui: scrolling up through a reply jumps the view into the thinking block above it #58666

Open

Description

@ganyicz

Description

When scrolling up through a session with thinking shown, the view jumps as the thinking block above a reply comes into view. A single wheel step skips the whole thinking block and lands on its "Thought" header, instead of moving a few rows like every other step. This happens every time a thinking block comes into view, and it makes it hard to keep your place while reading back through a session.

The screenshot shows three consecutive states, each one wheel step apart, left to right:

  1. Reading the reply to turn 19.
  2. One wheel step later, the view has moved 3 rows as expected. The "19. Cost of the limiter itself" heading is at the top.
  3. One more wheel step, and the view jumps about 23 rows to the start of turn 19's thinking. The whole thinking block is skipped.

I first noticed this in my normal setup. It reproduces on a clean install with no plugins, where the only config change is the thinking setting below.

Plugins

None (reproduced with no plugins)

OpenCode version

2.1.4

Steps to reproduce

  1. Set session.thinking to "show" in cli.json.
  2. Open a session with several turns where each assistant reply has a thinking block before it.
  3. Scroll up slowly from the bottom, one wheel step at a time.
  4. When the thinking block above a reply comes into view, the view jumps to the start of that thinking block in a single step.

Screenshot

Image

Operating System

macOS 27.0.1

Terminal

Ghostty 1.3.1

Activity

  1. opencode-agent commented on Oct 24, 2026

    @opencode-agent
    Contributor

    Thanks for the clear report! I could reproduce this on 2.1.4, and it still happens on the latest dev (f20760b8).

    Steps: set session.thinking to "show", create a session with 20 turns where each reply has a thinking block before it, then scroll up from the bottom one wheel step at a time. For the newest ~10 turns, each step moves ~3 rows. Further back, a single step goes from the first lines of a reply straight to the start of the thinking block above it, skipping the rest of the block, and the next step shows the "Thought" header. The recording below shows the full run, and the script below reproduces it.

    The jumps start where older messages begin to be loaded into the view as you scroll up, so the code that adds those rows above the current view is the likely cause. #54158 touches the same code and may be related.

    recording.mp4
    Reproduction script (drive.ts)
    // Repro for anomalyco/opencode#58666: scrolling up one wheel step at a time
    // jumps over the thinking block above a reply.
    import { Effect } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const TURNS = 20
    const thought = (n: number) =>
      Array.from({ length: 6 }, (_, k) => `Turn ${n} thought line ${k + 1}: weighing the options carefully.`).join("\n\n")
    const reply = (n: number) =>
      Array.from({ length: 10 }, (_, k) => `Turn ${n} reply line ${k + 1}.`).join("\n\n")
    
    // Text of the transcript area's rows, to see which line is at the top after each step.
    const frameText = (frame: any): string[] =>
      frame.lines.map((l: any) => l.spans.map((s: any) => s.text).join(""))
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tuiConfig: { session: { thinking: "show" } },
        tui: { recording: true, viewport: { cols: 120, rows: 40 } },
      } as any,
      ({ ui, llm }) =>
        Effect.gen(function* () {
          for (let n = 1; n <= TURNS; n++) {
            yield* ui.submit(`Question ${n}`)
            yield* llm.send(Llm.reasoning(thought(n)), Llm.text(reply(n)))
            yield* Effect.sleep(700)
          }
          yield* ui.screenshot("bottom")
          yield* Effect.sleep(1000)
          let prevTop = ""
          for (let step = 1; step <= 330; step++) {
            yield* ui.mouse({ action: "scroll", x: 40, y: 15, direction: "up" })
            yield* Effect.sleep(120)
            const lines = frameText(yield* ui.capture())
            const top = lines.slice(1, 4).map((l) => l.trim()).filter(Boolean).join(" | ")
            yield* Effect.log(`step ${step}: ${top.slice(0, 200)}`)
            prevTop = top
          }
          void prevTop
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

  2. Anthony14FR commented on Nov 12, 2026

    @Anthony14FR

    I'd like to work on this. Following the triage, the jump starts where older rows get loaded above the viewport while scrolling up, so I'll look at how the scroll offset is kept when those rows are prepended (and check #54158, which touches the same code). I'll add a regression test that scrolls up one step at a time through replies with thinking blocks.

  3. Anthony14FR commented on Nov 12, 2026

    @Anthony14FR

    Update: I can't reproduce this on current dev. I wrote an OpenTUI renderer test (20 and 30 turns, each with a reasoning part and a text part, session.thinking: "show", 100x30 and 120x40) that scrolls up one wheel step at a time and checks each step moves the view by exactly 3 rows. Every step does, including across the point where older rows get mounted.

    The triage repro was on f20760b8. Since then #59236 ("stop older history from flashing when scrolling up") changed pinAbove, the older-row reveal and rows.ts, which is likely what fixed it. The default verbosity is also low now (#63207), which folds thoughts into a collapsed group.

    My test loads the history in one fetch rather than streaming it live, so it doesn't cover everything the original script does. @ganyicz could you check whether it still happens on the latest release? If it does, I'll keep digging; if not, this can probably be closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions