Summary
The three-layer rendering architecture (PR #147) enforces the zone invariant: the renderer never writes more rows than screen.rows, preventing scrollback corruption. However, the renderer operates entirely on relative cursor movement -- it knows how many rows it wrote (zoneHeight) but not where those rows are in the terminal.
The Gap
Nothing in the renderer tracks the absolute position of the zone in the terminal. The render cycle is:
- Move cursor up by
zoneHeight (relative)
- Clear and redraw the zone
- Track
zoneHeight for next cycle
This works when the relationship between the cursor and the zone start is stable. But the terminal can mutate the visual layout of previously written content without notifying the application. When this happens, the tracked zoneHeight no longer reflects the actual visual row count, and relative cursor movement operates from the wrong position.
Observable behaviour
- The zone gets rendered in the wrong location (too high or too low)
- Previous zone content remains visible as a "ghost" -- not corruption, but visual duplication
- The severity depends on where the zone started in the terminal (fresh terminal vs mid-screen)
- The current narrow/widen heuristic in
notifyResize() mitigates the most common cases but is based on comparing old vs new zone heights, not on actual position knowledge
Root cause
The renderer has a relative model (I wrote N rows, move up N to erase) but operates in an absolute space (the terminal's row/column grid). There is no mechanism to reconcile the two when external factors change the mapping between them.
Constraints
- Scrollback history must be preserved (no alternate screen buffer, no scrollback clearing)
- The terminal owns the history -- the renderer only manages a dynamic zone at the bottom
- The zone invariant (never write more than
screen.rows) must be maintained
- Any solution should work in both bare terminals and tmux
Summary
The three-layer rendering architecture (PR #147) enforces the zone invariant: the renderer never writes more rows than
screen.rows, preventing scrollback corruption. However, the renderer operates entirely on relative cursor movement -- it knows how many rows it wrote (zoneHeight) but not where those rows are in the terminal.The Gap
Nothing in the renderer tracks the absolute position of the zone in the terminal. The render cycle is:
zoneHeight(relative)zoneHeightfor next cycleThis works when the relationship between the cursor and the zone start is stable. But the terminal can mutate the visual layout of previously written content without notifying the application. When this happens, the tracked
zoneHeightno longer reflects the actual visual row count, and relative cursor movement operates from the wrong position.Observable behaviour
notifyResize()mitigates the most common cases but is based on comparing old vs new zone heights, not on actual position knowledgeRoot cause
The renderer has a relative model (I wrote N rows, move up N to erase) but operates in an absolute space (the terminal's row/column grid). There is no mechanism to reconcile the two when external factors change the mapping between them.
Constraints
screen.rows) must be maintained