Skip to content

[Bug]: UI rapidly re-renders entire chat history from the beginning and snaps back to input field when scrolling up or typing / #533

Open

Description

@bormaa

Product or interface

CLI - interactive TUI

Version

0.6.12

Platform

Linux

OS version and architecture

Ubuntu 24.04 LTS

Issue area

Agent stuck / slow / repeated loop

Desktop log upload ID (optional)

No response

Steps to reproduce

Steps to Reproduce
Scenario 1: Scrolling Up

Open a terminal running zsh and start a tmux session.

Launch the mcode and generate enough chat history to exceed the height of the terminal window.

Attempt to scroll up (using the mouse wheel or tmux copy-mode keybindings).

Scenario 2: Typing a Slash (/) Command

In the same active chat session with existing history, focus on the input field.

Type / as the first character in the prompt and enter next character.

Expected and actual behavior

I should scroll chat history

Redacted error summary

Screenshots

Image

Before submitting

  • I have searched existing issues.
  • I have included my version and removed sensitive information.

Activity

  1. added
    bugSomething isn't working
    needs-triageAwaiting maintainer assessment
    on Oct 23, 2026
  2. pansz commented on Oct 26, 2026

    @pansz

    Confirmed on main (da22f11) — the symptoms in this report have a concrete architectural cause, and there is no viewport-level virtualization in the TUI.

    Core cause: the transcript is always rendered in full, and the off-screen part is discarded afterwards.

    • ScrollView has no viewport clip. packages/tui/src/tui/engine/components/scroll-view.ts:210 renders its entire child:
      override render(width: number): string[] {
        const contentWidth = this.getContentWidth(width);
        const lines = this.child.render(contentWidth);
    • The layout engine lays a scroll child out at its full content height and only then translates it by scrollTop (packages/tui/src/tui/engine/layout.ts:141-167); the clip is geometric and applied after rendering.
    • Fullscreen then throws the surplus away — packages/tui/src/tui/engine/tui-alt-screen.ts:1376: if (screen.length > height) screen = screen.slice(screen.length - height);
    • Regular mode has the same shape: packages/tui/src/tui/engine/tui-main-screen.ts:370 re-renders the whole document on every frame and only then diffs it against the previous frame.

    The only windowing that exists is at the projection level, not the screen level: TranscriptProjectionWindow caps the live projection at 30 turns / 120 cells per turn / 1000 cells (packages/tui/src/tui/transcript/projection-window.ts:44-48), and regular mode additionally retains at most 3000 final rows (APPEND_ONLY_RETAINED_ROWS, packages/tui/src/tui/transcript/view.ts:54). So the per-frame cost scales with the length of the conversation, not with the number of visible rows.

    Two independent cost centres, matching the two symptoms.

    Changing the composer height / editing the input. In regular mode the document is [final rows already in native scrollback] + [live rows] + [footer] (packages/tui/src/tui/shell/chat-layout.ts:343-391), and only the live region is clipped to the remaining budget. Changing the composer height moves that boundary, so the diff finds a change above the viewport origin (firstChanged < prevViewportTop, tui-main-screen.ts:705) and falls into reconstruct() → fullRender(true) → ED 2 + ED 3 + a replay of every line of the document (tui-main-screen.ts:474, 537-554). That is the "everything above re-renders" effect, and its cost scales with the retained document (~3000 rows), not with the screen height.

    Toggling "show details" (Ctrl+O — "Show or hide Thinking, Tool output, and diffs"). TranscriptView.toggleDetailMode() drops the frame cache outright (packages/tui/src/tui/transcript/view.ts:350-354). Since the per-unit cache key includes the resolved display mode (packages/tui/src/tui/transcript/view.ts:356-396), every unit misses, so all assistant Markdown blocks are re-parsed and re-rendered in one pass.

    Suggested direction. The fix is to virtualize one level earlier: clip at the layout level before rendering — render only [scrollTop, scrollTop + viewportHeight) — and let TranscriptView produce a projection anchored to the viewport instead of always projecting the trailing turns. TranscriptProjectionWindow already tracks an anchor cell (anchorCellId), so a scroll-anchored projection is a natural extension of the existing model. A cheaper partial mitigation: keep the full projection, but stop the composer-height path from triggering the ED 3 + full-document replay.

    Measuring it. PI_DEBUG_REDRAW=1 makes the regular-mode renderer log every redraw reason to ~/.pi/agent/pi-debug.log (packages/tui/src/tui/engine/tui-main-screen.ts:512-519). A fullRender: line versus a partial redraw tells the two cases apart immediately.

    Related: #481 (wheel no-op while following the tail), #535 (regular mode has no scrollback at all), #320/#348/#349 (follow-tail).

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingneeds-triageAwaiting maintainer assessmenttuiInteractive terminal UI (TUI)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions