You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: UI rapidly re-renders entire chat history from the beginning and snaps back to input field when scrolling up or typing / #533
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:
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).
Activity
pansz commentedon Oct 26, 2026
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.
ScrollViewhas no viewport clip.packages/tui/src/tui/engine/components/scroll-view.ts:210renders its entire child:scrollTop(packages/tui/src/tui/engine/layout.ts:141-167); the clip is geometric and applied after rendering.packages/tui/src/tui/engine/tui-alt-screen.ts:1376:if (screen.length > height) screen = screen.slice(screen.length - height);packages/tui/src/tui/engine/tui-main-screen.ts:370re-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:
TranscriptProjectionWindowcaps 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 intoreconstruct()→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 letTranscriptViewproduce a projection anchored to the viewport instead of always projecting the trailing turns.TranscriptProjectionWindowalready 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 theED 3+ full-document replay.Measuring it.
PI_DEBUG_REDRAW=1makes 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). AfullRender: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).